This commit is contained in:
parent
3ab3c21a09
commit
10a2733605
@ -8,10 +8,34 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/services/access"
|
||||
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestConfigAuthorizationAcceptsEitherRecoveryOption(t *testing.T) {
|
||||
actor := AuditActor{UserID: "2", Role: string(access.RoleAdmin), RecentAuth: true, ExternalEmailConfigured: true}
|
||||
if err := DefaultConfigAuthorization(stdcontext.Background(), actor, ConfigOperationUpdate, SectionPlugins); err != nil {
|
||||
t.Fatalf("email-only admin rejected: %v", err)
|
||||
}
|
||||
actor.RecentAuth = false
|
||||
if err := DefaultConfigAuthorization(stdcontext.Background(), actor, ConfigOperationUpdate, SectionPlugins); !access.IsDenied(err, access.CodeRecentAuthRequired) {
|
||||
t.Fatalf("email bypassed recent auth: %v", err)
|
||||
}
|
||||
actor.RecentAuth = true
|
||||
if err := DefaultConfigAuthorization(stdcontext.Background(), actor, ConfigOperationRollback, SectionRoot); !access.IsDenied(err, access.CodeAccessDenied) {
|
||||
t.Fatalf("email granted rollback: %v", err)
|
||||
}
|
||||
actor.ExternalEmailConfigured = false
|
||||
if err := DefaultConfigAuthorization(stdcontext.Background(), actor, ConfigOperationUpdate, SectionPlugins); !access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
||||
t.Fatalf("missing recovery option accepted: %v", err)
|
||||
}
|
||||
actor.RecoveryCodeConfigured = true
|
||||
if err := DefaultConfigAuthorization(stdcontext.Background(), actor, ConfigOperationUpdate, SectionPlugins); err != nil {
|
||||
t.Fatalf("code-only admin rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogFormatterHandlesMissingLogID(t *testing.T) {
|
||||
entry := &log.Entry{
|
||||
Context: &appcontext.Context{Context: stdcontext.Background()},
|
||||
|
||||
@ -55,12 +55,12 @@ type UpdateRequest struct {
|
||||
}
|
||||
|
||||
type AuditActor struct {
|
||||
UserID string
|
||||
Name string
|
||||
Role string
|
||||
SessionID string
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
UserID string
|
||||
Name string
|
||||
Role string
|
||||
SessionID string
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
ExternalEmailConfigured bool
|
||||
}
|
||||
|
||||
|
||||
@ -162,10 +162,10 @@ func DefaultConfigAuthorization(_ context.Context, actor AuditActor, operation C
|
||||
capability = access.CapabilityDatabaseMigrate
|
||||
}
|
||||
request := access.Request{
|
||||
ActorRole: access.Role(actor.Role),
|
||||
Capability: capability,
|
||||
RecentAuth: actor.RecentAuth,
|
||||
RecoveryCodeConfigured: actor.RecoveryCodeConfigured,
|
||||
ActorRole: access.Role(actor.Role),
|
||||
Capability: capability,
|
||||
RecentAuth: actor.RecentAuth,
|
||||
RecoveryCodeConfigured: actor.RecoveryCodeConfigured,
|
||||
ExternalEmailConfigured: actor.ExternalEmailConfigured,
|
||||
}
|
||||
if operation == ConfigOperationRollback && request.ActorRole == access.RoleSuperAdmin {
|
||||
|
||||
@ -16,18 +16,18 @@ func authorizeCapabilityWithNewRole(ctx *context.Context, capability access.Capa
|
||||
return &access.Denial{Code: access.CodeAccessDenied}
|
||||
}
|
||||
return access.Authorize(access.Request{
|
||||
ActorRole: ctx.EffectiveRole(),
|
||||
Capability: capability,
|
||||
TargetRole: targetRole,
|
||||
OwnResource: ownResource,
|
||||
RecentAuth: ctx.RecentAuth,
|
||||
RecoveryCodeConfigured: ctx.RecoveryCodeConfigured,
|
||||
ExternalEmailConfigured: ctx.ExternalEmailConfigured,
|
||||
TargetRecoveryConfigured: ctx.TargetRecoveryConfigured,
|
||||
ActorRole: ctx.EffectiveRole(),
|
||||
Capability: capability,
|
||||
TargetRole: targetRole,
|
||||
OwnResource: ownResource,
|
||||
RecentAuth: ctx.RecentAuth,
|
||||
RecoveryCodeConfigured: ctx.RecoveryCodeConfigured,
|
||||
ExternalEmailConfigured: ctx.ExternalEmailConfigured,
|
||||
TargetRecoveryConfigured: ctx.TargetRecoveryConfigured,
|
||||
TargetExternalEmailConfigured: ctx.TargetExternalEmailConfigured,
|
||||
NewRole: newRole,
|
||||
PluginInstanceAuthorized: ctx.PluginInstanceAuthorized,
|
||||
AuditScopeAuthorized: ctx.AuditScopeAuthorized,
|
||||
DeploymentChannel: ctx.DeploymentChannel,
|
||||
NewRole: newRole,
|
||||
PluginInstanceAuthorized: ctx.PluginInstanceAuthorized,
|
||||
AuditScopeAuthorized: ctx.AuditScopeAuthorized,
|
||||
DeploymentChannel: ctx.DeploymentChannel,
|
||||
})
|
||||
}
|
||||
|
||||
@ -71,6 +71,75 @@ func setupSecurityPromptTest(t *testing.T) *xorm.Engine {
|
||||
return engine
|
||||
}
|
||||
|
||||
func TestSecurityContextRecoveryOptionsRefresh(t *testing.T) {
|
||||
for _, role := range []access.Role{access.RoleAdmin, access.RoleUser} {
|
||||
t.Run(string(role), func(t *testing.T) {
|
||||
engine := setupSecurityPromptTest(t)
|
||||
user := models.User{Account: "option-user", Role: string(role)}
|
||||
if _, err := engine.Insert(&user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := &appcontext.Context{UserID: user.ID, Role: role, RecentAuth: true, RecoveryCodeConfigured: true, ExternalEmailConfigured: true}
|
||||
assertState := func(code, email bool) {
|
||||
t.Helper()
|
||||
LoadSecurityContext(ctx)
|
||||
if ctx.RecoveryCodeConfigured != code || ctx.ExternalEmailConfigured != email {
|
||||
t.Fatalf("unexpected context: code=%v email=%v", ctx.RecoveryCodeConfigured, ctx.ExternalEmailConfigured)
|
||||
}
|
||||
}
|
||||
assertState(false, false)
|
||||
email := models.ExternalEmail{UserID: user.ID, Status: models.ExternalEmailStatusPending, EmailFingerprint: "fixture"}
|
||||
if _, err := engine.Insert(&email); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertState(false, false)
|
||||
if _, err := engine.ID(email.ID).Cols("status").Update(&models.ExternalEmail{Status: models.ExternalEmailStatusVerified}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertState(false, true)
|
||||
if role == access.RoleAdmin {
|
||||
if err := authorizeCapability(ctx, access.CapabilityPasswordReset, access.RoleUser, false); err != nil {
|
||||
t.Fatalf("email state not forwarded: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err := engine.ID(email.ID).Cols("status").Update(&models.ExternalEmail{Status: models.ExternalEmailStatusDisabled}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertState(false, false)
|
||||
code := models.RecoveryCode{UserID: user.ID, CodeHash: "fixture"}
|
||||
if _, err := engine.Insert(&code); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertState(true, false)
|
||||
now := time.Now()
|
||||
if _, err := engine.ID(code.ID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertState(false, false)
|
||||
ctx.UserID = 0
|
||||
ctx.ExternalEmailConfigured, ctx.RecoveryCodeConfigured = true, true
|
||||
assertState(false, false)
|
||||
ctx.UserID = user.ID
|
||||
db.Instance = nil
|
||||
ctx.ExternalEmailConfigured, ctx.RecoveryCodeConfigured = true, true
|
||||
assertState(false, false)
|
||||
})
|
||||
}
|
||||
LoadSecurityContext(nil)
|
||||
}
|
||||
|
||||
func TestRoleAuthorizationForwardsTargetEmail(t *testing.T) {
|
||||
ctx := &appcontext.Context{UserID: 1, Role: access.RoleSuperAdmin, RecentAuth: true, TargetExternalEmailConfigured: true}
|
||||
if err := authorizeCapabilityWithNewRole(ctx, access.CapabilityRoleManage, access.RoleUser, access.RoleAdmin, false); err != nil {
|
||||
t.Fatalf("target email was not forwarded: %v", err)
|
||||
}
|
||||
ctx.TargetExternalEmailConfigured = false
|
||||
ctx.ExternalEmailConfigured = true
|
||||
if err := authorizeCapabilityWithNewRole(ctx, access.CapabilityRoleManage, access.RoleUser, access.RoleAdmin, false); !access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
||||
t.Fatalf("actor state substituted for target: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecuritySetupAcknowledgementIsSessionScoped(t *testing.T) {
|
||||
engine := setupSecurityPromptTest(t)
|
||||
previousSession, previousConfig, previousManager := session.Instance, config.Instance, config.CurrentManager()
|
||||
|
||||
@ -2,6 +2,63 @@ package access
|
||||
|
||||
import "testing"
|
||||
|
||||
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
|
||||
|
||||
@ -25,19 +25,19 @@ func IsDenied(err error, code DenialCode) bool {
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
ActorRole Role
|
||||
Capability Capability
|
||||
TargetRole Role
|
||||
NewRole Role
|
||||
OwnResource bool
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
ExternalEmailConfigured bool
|
||||
TargetRecoveryConfigured bool
|
||||
ActorRole Role
|
||||
Capability Capability
|
||||
TargetRole Role
|
||||
NewRole Role
|
||||
OwnResource bool
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
ExternalEmailConfigured bool
|
||||
TargetRecoveryConfigured bool
|
||||
TargetExternalEmailConfigured bool
|
||||
PluginInstanceAuthorized bool
|
||||
AuditScopeAuthorized bool
|
||||
DeploymentChannel bool
|
||||
PluginInstanceAuthorized bool
|
||||
AuditScopeAuthorized bool
|
||||
DeploymentChannel bool
|
||||
}
|
||||
|
||||
func Authorize(request Request) error {
|
||||
|
||||
@ -11,26 +11,26 @@ const (
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
context.Context `json:"-"`
|
||||
UserID int
|
||||
SessionVersion int
|
||||
SecurityVersion int
|
||||
UserAccount string
|
||||
UserName string
|
||||
Role access.Role
|
||||
Values map[string]any
|
||||
Lang string
|
||||
IsAdmin bool
|
||||
MustChangePassword int
|
||||
SecuritySetupAcknowledged int
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
ExternalEmailConfigured bool
|
||||
TargetRecoveryConfigured bool
|
||||
context.Context `json:"-"`
|
||||
UserID int
|
||||
SessionVersion int
|
||||
SecurityVersion int
|
||||
UserAccount string
|
||||
UserName string
|
||||
Role access.Role
|
||||
Values map[string]any
|
||||
Lang string
|
||||
IsAdmin bool
|
||||
MustChangePassword int
|
||||
SecuritySetupAcknowledged int
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
ExternalEmailConfigured bool
|
||||
TargetRecoveryConfigured bool
|
||||
TargetExternalEmailConfigured bool
|
||||
PluginInstanceAuthorized bool
|
||||
AuditScopeAuthorized bool
|
||||
DeploymentChannel bool
|
||||
PluginInstanceAuthorized bool
|
||||
AuditScopeAuthorized bool
|
||||
DeploymentChannel bool
|
||||
}
|
||||
|
||||
// HasRole 判断当前请求是否属于指定角色。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user