246 lines
11 KiB
Go
246 lines
11 KiB
Go
package access
|
|
|
|
import "testing"
|
|
|
|
func TestHasRecoveryOptionAcceptsEitherConfiguredMethod(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
code bool
|
|
email bool
|
|
want bool
|
|
}{
|
|
{name: "neither", want: false},
|
|
{name: "security code only", code: true, want: true},
|
|
{name: "external email only", email: true, want: true},
|
|
{name: "both", code: true, email: true, want: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := HasRecoveryOption(test.code, test.email); got != test.want {
|
|
t.Fatalf("HasRecoveryOption(%t, %t) = %t, want %t", test.code, test.email, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecoveryOptionAuthorization(t *testing.T) {
|
|
for _, capability := range []Capability{CapabilityPasswordReset, CapabilityUserSecurityManage, CapabilityNotificationManage} {
|
|
for _, ready := range []struct {
|
|
name string
|
|
code bool
|
|
email bool
|
|
}{{"neither", false, false}, {"code", true, false}, {"email", false, true}, {"both", true, true}} {
|
|
t.Run(string(capability)+"/"+ready.name, func(t *testing.T) {
|
|
req := Request{ActorRole: RoleAdmin, TargetRole: RoleUser, Capability: capability, RecentAuth: true,
|
|
RecoveryCodeConfigured: ready.code, ExternalEmailConfigured: ready.email}
|
|
err := Authorize(req)
|
|
if ready.code || ready.email {
|
|
if err != nil {
|
|
t.Fatalf("expected allow: %v", err)
|
|
}
|
|
} else if !IsDenied(err, CodeRecoveryCodeRequired) {
|
|
t.Fatalf("expected recovery requirement: %v", err)
|
|
}
|
|
req.RecentAuth = false
|
|
if err := Authorize(req); !IsDenied(err, CodeRecentAuthRequired) {
|
|
t.Fatalf("missing recent auth accepted: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
for _, target := range []Role{RoleAdmin, RoleSuperAdmin} {
|
|
req := Request{ActorRole: RoleAdmin, TargetRole: target, Capability: CapabilityPasswordReset, RecentAuth: true, ExternalEmailConfigured: true}
|
|
if err := Authorize(req); !IsDenied(err, CodeTargetRoleDenied) {
|
|
t.Fatalf("email bypassed target boundary: %v", err)
|
|
}
|
|
}
|
|
req := Request{ActorRole: RoleUser, TargetRole: RoleUser, Capability: CapabilityNotificationManage, RecentAuth: true, ExternalEmailConfigured: true, RecoveryCodeConfigured: true}
|
|
if err := Authorize(req); !IsDenied(err, CodeAccessDenied) {
|
|
t.Fatalf("user received management permission: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRoleChangeAcceptsTargetEmailWithoutBorrowingActorState(t *testing.T) {
|
|
req := Request{ActorRole: RoleSuperAdmin, Capability: CapabilityRoleManage, TargetRole: RoleUser, NewRole: RoleAdmin, RecentAuth: true, ExternalEmailConfigured: true}
|
|
if err := Authorize(req); !IsDenied(err, CodeRecoveryCodeRequired) {
|
|
t.Fatalf("actor email substituted for target: %v", err)
|
|
}
|
|
req.TargetExternalEmailConfigured = true
|
|
if err := Authorize(req); err != nil {
|
|
t.Fatalf("target email did not qualify: %v", err)
|
|
}
|
|
req.NewRole = RoleUser
|
|
req.TargetRole = RoleAdmin
|
|
if err := Authorize(req); err != nil {
|
|
t.Fatalf("email-only downgrade rejected: %v", err)
|
|
}
|
|
req.RecentAuth = false
|
|
if err := Authorize(req); !IsDenied(err, CodeRecentAuthRequired) {
|
|
t.Fatalf("role change bypassed recent auth: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultDeny(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
req Request
|
|
code DenialCode
|
|
}{
|
|
{name: "invalid role", req: Request{ActorRole: Role("root"), Capability: CapabilityConfigRead}, code: CodeInvalidRole},
|
|
{name: "unknown capability", req: Request{ActorRole: RoleSuperAdmin, Capability: Capability("config.delete")}, code: CodeUnknownCapability},
|
|
{name: "missing grant", req: Request{ActorRole: RoleUser, Capability: CapabilityConfigRollback}, code: CodeAccessDenied},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if err := Authorize(test.req); !IsDenied(err, test.code) {
|
|
t.Fatalf("expected %s, got %v", test.code, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRoleBoundaries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
req Request
|
|
code DenialCode
|
|
}{
|
|
{name: "super admin resets admin", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityPasswordReset, TargetRole: RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true}},
|
|
{name: "super admin cannot be web reset target", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityPasswordReset, TargetRole: RoleSuperAdmin, RecentAuth: true, RecoveryCodeConfigured: true}, code: CodeTargetRoleDenied},
|
|
{name: "admin resets user", req: Request{ActorRole: RoleAdmin, Capability: CapabilityPasswordReset, TargetRole: RoleUser, RecentAuth: true, RecoveryCodeConfigured: true}},
|
|
{name: "admin cannot reset admin", req: Request{ActorRole: RoleAdmin, Capability: CapabilityPasswordReset, TargetRole: RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true}, code: CodeTargetRoleDenied},
|
|
{name: "user cannot reset another user", req: Request{ActorRole: RoleUser, Capability: CapabilityPasswordReset, TargetRole: RoleUser, RecentAuth: true, RecoveryCodeConfigured: true}, code: CodeAccessDenied},
|
|
{name: "super admin manages user security", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityUserSecurityManage, TargetRole: RoleUser, RecentAuth: true}},
|
|
{name: "admin manages user security", req: Request{ActorRole: RoleAdmin, Capability: CapabilityUserSecurityManage, TargetRole: RoleUser, RecentAuth: true, RecoveryCodeConfigured: true}},
|
|
{name: "admin cannot manage admin security", req: Request{ActorRole: RoleAdmin, Capability: CapabilityUserSecurityManage, TargetRole: RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true}, code: CodeTargetRoleDenied},
|
|
{name: "web cannot manage super admin security", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityUserSecurityManage, TargetRole: RoleSuperAdmin, RecentAuth: true}, code: CodeTargetRoleDenied},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := Authorize(test.req)
|
|
if test.code == "" && err != nil {
|
|
t.Fatalf("expected allow, got %v", err)
|
|
}
|
|
if test.code != "" && !IsDenied(err, test.code) {
|
|
t.Fatalf("expected %s, got %v", test.code, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecoveryAndRecentAuthentication(t *testing.T) {
|
|
admin := Request{ActorRole: RoleAdmin, Capability: CapabilityPluginWriteGlobal, RecentAuth: true}
|
|
if err := Authorize(admin); !IsDenied(err, CodeRecoveryCodeRequired) {
|
|
t.Fatalf("expected recovery code requirement, got %v", err)
|
|
}
|
|
admin.RecoveryCodeConfigured = true
|
|
admin.RecentAuth = false
|
|
if err := Authorize(admin); !IsDenied(err, CodeRecentAuthRequired) {
|
|
t.Fatalf("expected recent authentication requirement, got %v", err)
|
|
}
|
|
superAdmin := Request{ActorRole: RoleSuperAdmin, Capability: CapabilityConfigRollback, RecentAuth: true}
|
|
if err := Authorize(superAdmin); err != nil {
|
|
t.Fatalf("super admin should be exempt from recovery code precondition: %v", err)
|
|
}
|
|
adminRollback := Request{ActorRole: RoleAdmin, Capability: CapabilityConfigRollback, RecentAuth: true}
|
|
if err := Authorize(adminRollback); !IsDenied(err, CodeAccessDenied) {
|
|
t.Fatalf("admin must not receive a recovery-code bypass for a super-admin capability: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRoleChangeRequiresTargetRecovery(t *testing.T) {
|
|
req := Request{ActorRole: RoleSuperAdmin, Capability: CapabilityRoleManage, TargetRole: RoleUser, NewRole: RoleAdmin, RecentAuth: true, RecoveryCodeConfigured: true}
|
|
if err := Authorize(req); !IsDenied(err, CodeRecoveryCodeRequired) {
|
|
t.Fatalf("expected target recovery requirement, got %v", err)
|
|
}
|
|
req.TargetRecoveryConfigured = true
|
|
if err := Authorize(req); err != nil {
|
|
t.Fatalf("expected role change to be allowed, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAdminPluginRestartScope(t *testing.T) {
|
|
req := Request{ActorRole: RoleAdmin, Capability: CapabilityPluginRestart, RecentAuth: true, RecoveryCodeConfigured: true}
|
|
if err := Authorize(req); !IsDenied(err, CodeAccessDenied) {
|
|
t.Fatalf("expected plugin scope denial, got %v", err)
|
|
}
|
|
req.PluginInstanceAuthorized = true
|
|
if err := Authorize(req); !IsDenied(err, CodeAccessDenied) {
|
|
t.Fatalf("expected plugin restart to remain denied, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManagementEndpointCatalog(t *testing.T) {
|
|
endpoints := ManagementEndpoints()
|
|
if len(endpoints) != 47 {
|
|
t.Fatalf("unexpected endpoint count: %d", len(endpoints))
|
|
}
|
|
seen := map[string]struct{}{}
|
|
for _, endpoint := range endpoints {
|
|
key := endpoint.Method + " " + endpoint.Path
|
|
if _, ok := seen[key]; ok {
|
|
t.Fatalf("duplicate endpoint: %s", key)
|
|
}
|
|
seen[key] = struct{}{}
|
|
if _, ok := GetPolicy(endpoint.Capability); !ok {
|
|
t.Fatalf("endpoint %s references unknown capability", key)
|
|
}
|
|
}
|
|
endpoint, ok := LookupEndpoint("POST", "/api/plugin/settings/example")
|
|
if !ok || endpoint.Capability != CapabilityPluginReadGlobal {
|
|
t.Fatal("plugin settings endpoint was not matched")
|
|
}
|
|
endpoint, ok = LookupEndpoint("GET", "/api/v1/logs")
|
|
if !ok || endpoint.Capability != CapabilityAuditRead {
|
|
t.Fatal("log endpoint was not matched")
|
|
}
|
|
endpoint, ok = LookupEndpoint("POST", "/api/v1/config/smtp/validate")
|
|
if !ok || endpoint.Capability != CapabilityConfigWriteGlobal {
|
|
t.Fatal("configuration validation endpoint was not matched")
|
|
}
|
|
endpoint, ok = LookupEndpoint("POST", "/api/v1/system/restart")
|
|
if !ok || endpoint.Capability != CapabilityServiceRestart {
|
|
t.Fatal("system restart endpoint was not matched")
|
|
}
|
|
endpoint, ok = LookupEndpoint("GET", "/api/v1/config/email")
|
|
if !ok || endpoint.Capability != CapabilityConfigRead {
|
|
t.Fatal("configuration section endpoint was not matched")
|
|
}
|
|
endpoint, ok = LookupEndpoint("PUT", "/api/v1/config/email")
|
|
if !ok || endpoint.Capability != CapabilityConfigWriteGlobal {
|
|
t.Fatal("configuration update endpoint was not matched")
|
|
}
|
|
endpoint, ok = LookupEndpoint("GET", "/api/v1/config/backups")
|
|
if !ok || endpoint.Capability != CapabilityConfigRollback {
|
|
t.Fatal("configuration backup endpoint did not require rollback capability")
|
|
}
|
|
endpoint, ok = LookupEndpoint("POST", "/api/v1/user/security/external-email/change")
|
|
if !ok || endpoint.Capability != CapabilityUserSecurityManage {
|
|
t.Fatal("managed user security endpoint was not matched")
|
|
}
|
|
}
|
|
|
|
func TestEveryManagementEndpointHasAllowAndDeny(t *testing.T) {
|
|
for _, endpoint := range ManagementEndpoints() {
|
|
actorRole := RoleSuperAdmin
|
|
allowed := Request{
|
|
ActorRole: actorRole,
|
|
Capability: endpoint.Capability,
|
|
TargetRole: RoleUser,
|
|
OwnResource: true,
|
|
RecentAuth: true,
|
|
RecoveryCodeConfigured: true,
|
|
TargetRecoveryConfigured: true,
|
|
PluginInstanceAuthorized: true,
|
|
}
|
|
if err := Authorize(allowed); err != nil {
|
|
t.Fatalf("expected allow for %s %s: %v", endpoint.Method, endpoint.Path, err)
|
|
}
|
|
denied := allowed
|
|
denied.ActorRole = Role("unknown")
|
|
if err := Authorize(denied); !IsDenied(err, CodeInvalidRole) {
|
|
t.Fatalf("expected deny for %s %s, got %v", endpoint.Method, endpoint.Path, err)
|
|
}
|
|
}
|
|
}
|