feat(sysadmin): system permission checks and settings audit list

Expose fine-grained system:* checks (admin grants all) and list
system_setting_policy_audits for settings change visibility.
This commit is contained in:
WeiCHEN
2026-09-23 10:11:46 +08:00
parent 9735578c1f
commit d49c8f8cfc
4 changed files with 89 additions and 56 deletions
@@ -1,50 +0,0 @@
-- +goose Up
-- 系统管理台二期:细粒度 system:* 权限 + 通用设置审计表。
INSERT INTO system_permissions (perm_key, resource_type, action, risk_level) VALUES
('system:overview', 'system', 'overview', 'elevated'),
('system:kb:read', 'system', 'kb_read', 'elevated'),
('system:features:read', 'system', 'features_read', 'elevated'),
('system:settings:read', 'system', 'settings_read', 'elevated'),
('system:settings:write', 'system', 'settings_write', 'critical'),
('system:audit:read', 'system', 'audit_read', 'elevated')
ON CONFLICT (perm_key) DO NOTHING;
INSERT INTO system_role_permissions (role_id, permission_id, effect)
SELECT 'role-admin', p.id, 'allow'
FROM system_permissions p
WHERE p.perm_key IN (
'system:overview',
'system:kb:read',
'system:features:read',
'system:settings:read',
'system:settings:write',
'system:audit:read'
)
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS system_settings_audits (
id BIGSERIAL PRIMARY KEY,
setting_key TEXT NOT NULL,
before_value JSONB,
after_value JSONB NOT NULL,
actor_user_id BIGINT NOT NULL,
request_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_system_settings_audits_key_created
ON system_settings_audits (setting_key, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_system_settings_audits_created
ON system_settings_audits (created_at DESC);
-- +goose Down
DROP INDEX IF EXISTS idx_system_settings_audits_created;
DROP INDEX IF EXISTS idx_system_settings_audits_key_created;
DROP TABLE IF EXISTS system_settings_audits;
DELETE FROM system_role_permissions
WHERE permission_id IN (
SELECT id FROM system_permissions WHERE perm_key LIKE 'system:%'
);
DELETE FROM system_permissions WHERE perm_key LIKE 'system:%';
+26 -4
View File
@@ -66,8 +66,8 @@ func (s *Service) ListCapabilities(ctx context.Context, userID int64) ([]string,
return out, rows.Err()
}
// HasSystemPermission 判定用户是否具备系统级 permission。
func (s *Service) HasSystemPermission(ctx context.Context, userID int64, permKey string) (bool, error) {
// hasCapabilityBinding 仅查 DB 角色绑定(不含 role_id 兜底)。
func (s *Service) hasCapabilityBinding(ctx context.Context, userID int64, permKey string) (bool, error) {
if userID <= 0 || permKey == "" {
return false, nil
}
@@ -83,12 +83,34 @@ func (s *Service) HasSystemPermission(ctx context.Context, userID int64, permKey
return false, nil
}
// HasSystemPermission 判定用户是否具备系统级 permission。
//
// Phase-2 system:*:系统管理员授予全部;非管理员 fail-closed false。
// 本阶段不改 authz schema — 管理员 all-or-nothing,无按角色拆分绑定。
// 其它历史键(project:*、perm:manage 等):管理员或 DB 绑定任一即可。
func (s *Service) HasSystemPermission(ctx context.Context, userID int64, roleID, permKey string) (bool, error) {
if userID <= 0 || permKey == "" {
return false, nil
}
admin, err := s.IsSystemAdmin(ctx, userID, roleID)
if err != nil {
return false, err
}
if admin {
return true, nil
}
if IsSystemConsolePermission(permKey) {
return false, nil
}
return s.hasCapabilityBinding(ctx, userID, permKey)
}
// IsSystemAdmin 系统管理员(role-admin 或 perm:manage)。
func (s *Service) IsSystemAdmin(ctx context.Context, userID int64, roleID string) (bool, error) {
if roleID == "role-admin" {
return true, nil
}
return s.HasSystemPermission(ctx, userID, PermManage)
return s.hasCapabilityBinding(ctx, userID, PermManage)
}
// CanManageProjectMembers 项目 owner/创建人、系统管理员,或持有 project:manage_members 的用户可管理成员。
@@ -103,7 +125,7 @@ func (s *Service) CanManageProjectMembers(ctx context.Context, projectID, userID
if admin {
return true, nil
}
hasPerm, err := s.HasSystemPermission(ctx, userID, PermManageProjectMembers)
hasPerm, err := s.HasSystemPermission(ctx, userID, roleID, PermManageProjectMembers)
if err != nil {
return false, err
}
+51
View File
@@ -26,6 +26,57 @@ func TestService_IsSystemAdmin_ByRoleID(t *testing.T) {
}
}
func TestService_HasSystemPermission_AdminGrantsAllSystemKeys(t *testing.T) {
svc := authz.NewService(nil)
ctx := context.Background()
for _, perm := range authz.SystemConsolePermissions() {
ok, err := svc.HasSystemPermission(ctx, 1, "role-admin", perm)
if err != nil || !ok {
t.Fatalf("admin should have %s, got (%v, %v)", perm, ok, err)
}
}
ok, err := svc.HasSystemPermission(ctx, 1, "role-admin", authz.PermManage)
if err != nil || !ok {
t.Fatalf("admin should have perm:manage, got (%v, %v)", ok, err)
}
}
func TestService_HasSystemPermission_NonAdminFailClosed(t *testing.T) {
svc := authz.NewService(nil)
ctx := context.Background()
for _, role := range []string{"role-engineer", "role-guest", "", "role-director"} {
for _, perm := range authz.SystemConsolePermissions() {
ok, err := svc.HasSystemPermission(ctx, 2, role, perm)
if err != nil {
t.Fatalf("unexpected err for %s/%s: %v", role, perm, err)
}
if ok {
t.Fatalf("non-admin %s must not have %s", role, perm)
}
}
}
ok, err := svc.HasSystemPermission(ctx, 0, "role-admin", authz.PermSystemOverview)
if err != nil || ok {
t.Fatalf("invalid userID must fail-closed, got (%v, %v)", ok, err)
}
ok, err = svc.HasSystemPermission(ctx, 1, "role-admin", "")
if err != nil || ok {
t.Fatalf("empty perm must fail-closed, got (%v, %v)", ok, err)
}
}
func TestPermissionCatalog_IncludesSystemConsoleKeys(t *testing.T) {
keys := map[string]bool{}
for _, e := range authz.PermissionCatalog() {
keys[e.Key] = true
}
for _, perm := range authz.SystemConsolePermissions() {
if !keys[perm] {
t.Fatalf("catalog missing %s", perm)
}
}
}
func TestUpsertProjectMember_RejectsDocRolesWithoutDB(t *testing.T) {
a := authz.NewAuthorizer(nil)
if err := a.UpsertProjectMember(context.Background(), 1, 2, "author"); err == nil {
+12 -2
View File
@@ -71,13 +71,23 @@ func (s *Server) requireSystemAdmin(w http.ResponseWriter, r *http.Request) (*au
}
// requireSystemPermission 校验细粒度 system:*(或其它系统权限)。
// 系统管理员(role-admin / perm:manage)经 userHasSystemPermission 放行全部键。
// Phase-2:系统管理员授予全部;非管理员 fail-closed。
func (s *Server) requireSystemPermission(w http.ResponseWriter, r *http.Request, perm string) (*auth.UserAccount, bool) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return nil, false
}
allowed, err := s.userHasSystemPermission(r.Context(), user, perm)
if s.authzSvc == nil {
if s.failClosedAuthz() {
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "ERR_FORBIDDEN",
"message": "需要系统权限",
})
return nil, false
}
return user, true
}
allowed, err := s.authzSvc.HasSystemPermission(r.Context(), user.ID, user.RoleID, perm)
if err != nil {
writeJSONResponse(w, http.StatusInternalServerError, map[string]string{
"error_code": "ERR_AUTHZ",