panel-redesign: inbound is delegated per user, separately from outbound
Stage 1, step 2 (plan § Who sees what).
Store
- Reach{AllDomains, DomainIDs, AllInbound, InboundDomainIDs} is what a
domain user is assigned. CreateUser takes it; SetUserReach / ClearUserReach
replace SetUserDomains / ClearUserDomains and write both directions in one
transaction. A domain user with nothing on either list is refused
(ErrEmptyReach). With All set the rows of that direction are not stored,
so clearing All later brings nothing stale back.
- ListDomainsForUser honours all_domains; ListInboundDomainsForUser is its
inbound twin. Both filter in the query.
Authorization
- auth.Principal carries the whole reach: CanAccessDomain (now also true
with All), CanAccessInboundDomain, HasOutbound, HasInbound. An id or a
name on one list opens nothing on the other.
- Inbound handlers in three strengths: requireInbound (the list, filtered),
requireInboundDomain (one domain: page, DNS re-check, upstream,
recipients), requireInboundGlobal (add, delete confirm, delete). All
answer the same 404.
- domainIDSet and the DMARC report pages go through the principal instead
of looking at p.Domains themselves.
Panel (old templates, until stage 2)
- User form: two lists with All; inbound is offered only where the feature
is on, and saving the form with it off keeps the user's inbound reach.
Promotion to global drops the reach.
- Menu: a group with nothing assigned is absent; / sends an inbound-only
administrator to Inbound. The inbound pages hide add / delete from a
domain administrator.
Tests: every route of one inbound domain against five kinds of refused
request (another tenant's id, a missing id, outbound-only reach, an outbound
domain with the same id, one with the same name, no principal) with the 404
bodies compared; add/delete as every non-global user; the filtered list
including a domain added later; thirteen outbound routes against another
tenant's domain and application id; the user form; the reach in the store;
the principal matrix; the menu; the home redirect. Removing the access check
or the query filter makes the tests fail (checked by hand).
go test ./... green except the two Windows-only failures in internal/domain
and internal/logtail that predate this work.
Roadmap 12/27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,12 @@ type Principal struct {
|
||||
ID int64
|
||||
Username string
|
||||
Role Role
|
||||
Domains []int64 // assigned domain IDs; empty for global (all domains)
|
||||
// What a domain user reaches (store.Reach). All four are unused for the
|
||||
// global role, which reaches everything.
|
||||
Domains []int64 // assigned outbound domain ids
|
||||
AllDomains bool // every outbound domain, including those added later
|
||||
InboundDomains []int64 // assigned inbound domain ids
|
||||
AllInbound bool // every inbound domain, including those added later
|
||||
}
|
||||
|
||||
// IsGlobal reports whether the principal has full panel access.
|
||||
@@ -35,9 +40,35 @@ func (p Principal) IsGlobal() bool {
|
||||
return p.Role == RoleGlobal
|
||||
}
|
||||
|
||||
// CanAccessDomain reports whether the principal may access a domain id.
|
||||
// HasOutbound reports whether the principal reaches any sending domain. A
|
||||
// group with nothing in it is absent from that user's menu.
|
||||
func (p Principal) HasOutbound() bool {
|
||||
return p.IsGlobal() || p.AllDomains || len(p.Domains) > 0
|
||||
}
|
||||
|
||||
// HasInbound reports whether the principal reaches any inbound domain.
|
||||
func (p Principal) HasInbound() bool {
|
||||
return p.IsGlobal() || p.AllInbound || len(p.InboundDomains) > 0
|
||||
}
|
||||
|
||||
// CanAccessInboundDomain reports whether the principal may access an inbound
|
||||
// domain id. Inbound is granted separately from outbound: a sending domain of
|
||||
// the same name gives no access here, and the other way round.
|
||||
func (p Principal) CanAccessInboundDomain(inboundDomainID int64) bool {
|
||||
if p.IsGlobal() || p.AllInbound {
|
||||
return true
|
||||
}
|
||||
for _, id := range p.InboundDomains {
|
||||
if id == inboundDomainID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CanAccessDomain reports whether the principal may access a sending domain id.
|
||||
func (p Principal) CanAccessDomain(domainID int64) bool {
|
||||
if p.IsGlobal() {
|
||||
if p.IsGlobal() || p.AllDomains {
|
||||
return true
|
||||
}
|
||||
for _, id := range p.Domains {
|
||||
@@ -59,6 +90,10 @@ func principalFromUser(u store.User) Principal {
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
Domains: u.DomainIDs,
|
||||
|
||||
AllDomains: u.AllDomains,
|
||||
InboundDomains: u.InboundDomainIDs,
|
||||
AllInbound: u.AllInboundDomains,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user