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:
@@ -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:%';
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user