Add files via upload

This commit is contained in:
公明
2026-07-10 16:51:06 +08:00
committed by GitHub
parent 542d1d2411
commit bc3246d157
8 changed files with 841 additions and 22 deletions
+183 -17
View File
@@ -1,11 +1,14 @@
package security
import (
"database/sql"
"errors"
"strings"
"sync"
"time"
"cyberstrike-ai/internal/database"
"github.com/google/uuid"
)
@@ -16,14 +19,21 @@ var (
// Session represents an authenticated user session.
type Session struct {
Token string
ExpiresAt time.Time
Token string
ExpiresAt time.Time
UserID string
Username string
DisplayName string
Roles []string
Permissions map[string]bool
Scope string
}
// AuthManager manages password-based authentication and session lifecycle.
type AuthManager struct {
password string
sessionDuration time.Duration
db *database.DB
mu sync.RWMutex
sessions map[string]Session
@@ -46,23 +56,94 @@ func NewAuthManager(password string, sessionDurationHours int) (*AuthManager, er
}, nil
}
// Authenticate validates the password and creates a new session.
func (a *AuthManager) Authenticate(password string) (string, time.Time, error) {
if password != a.password {
return "", time.Time{}, ErrInvalidPassword
// AttachRBACStore enables multi-user RBAC authentication and bootstraps the
// built-in admin account from the legacy auth.password value.
func (a *AuthManager) AttachRBACStore(db *database.DB) error {
if db == nil {
return nil
}
hash, err := HashPassword(a.password)
if err != nil {
return err
}
if err := db.BootstrapRBAC(hash, PermissionCatalog); err != nil {
return err
}
a.mu.Lock()
a.db = db
a.mu.Unlock()
return nil
}
// Authenticate validates the password and creates a new session.
func (a *AuthManager) Authenticate(username, password string) (string, time.Time, error) {
session, err := a.authenticateSession(username, password)
if err != nil {
return "", time.Time{}, err
}
a.mu.Lock()
a.sessions[session.Token] = session
a.mu.Unlock()
return session.Token, session.ExpiresAt, nil
}
func (a *AuthManager) authenticateSession(username, password string) (Session, error) {
token := uuid.NewString()
expiresAt := time.Now().Add(a.sessionDuration)
a.mu.Lock()
a.sessions[token] = Session{
Token: token,
ExpiresAt: expiresAt,
}
a.mu.Unlock()
a.mu.RLock()
db := a.db
legacyPassword := a.password
a.mu.RUnlock()
return token, expiresAt, nil
if db == nil {
if password != legacyPassword {
return Session{}, ErrInvalidPassword
}
return Session{
Token: token,
ExpiresAt: expiresAt,
UserID: "admin",
Username: "admin",
DisplayName: "管理员",
Roles: []string{database.RBACSystemRoleAdmin},
Permissions: allPermissions(),
Scope: database.RBACScopeAll,
}, nil
}
username = strings.TrimSpace(strings.ToLower(username))
if username == "" {
username = "admin"
}
user, err := db.GetRBACUserByUsername(username)
if err != nil {
if err == sql.ErrNoRows {
return Session{}, ErrInvalidPassword
}
return Session{}, err
}
if !user.Enabled || !VerifyPasswordHash(password, user.PasswordHash) {
return Session{}, ErrInvalidPassword
}
access, err := db.ResolveRBACAccess(user.ID)
if err != nil {
return Session{}, err
}
roleIDs := make([]string, 0, len(access.Roles))
for _, role := range access.Roles {
roleIDs = append(roleIDs, role.ID)
}
return Session{
Token: token,
ExpiresAt: expiresAt,
UserID: user.ID,
Username: user.Username,
DisplayName: user.DisplayName,
Roles: roleIDs,
Permissions: access.Permissions,
Scope: access.Scope,
}, nil
}
// ValidateToken checks whether the provided token is still valid.
@@ -90,9 +171,51 @@ func (a *AuthManager) ValidateToken(token string) (Session, bool) {
// CheckPassword verifies whether the provided password matches the current password.
func (a *AuthManager) CheckPassword(password string) bool {
return a.CheckUserPassword("admin", password)
}
// CheckUserPassword verifies whether the provided password matches a user.
func (a *AuthManager) CheckUserPassword(username, password string) bool {
a.mu.RLock()
defer a.mu.RUnlock()
return password == a.password
db := a.db
legacyPassword := a.password
a.mu.RUnlock()
if db == nil {
return password == legacyPassword
}
user, err := db.GetRBACUserByUsername(username)
if err != nil {
return false
}
return VerifyPasswordHash(password, user.PasswordHash)
}
func (a *AuthManager) UpdateUserPassword(userID, password string) error {
password = strings.TrimSpace(password)
if password == "" {
return errors.New("auth password must be configured")
}
hash, err := HashPassword(password)
if err != nil {
return err
}
a.mu.RLock()
db := a.db
a.mu.RUnlock()
if db == nil {
return a.UpdateConfig(password, a.SessionDurationHours())
}
if err := db.UpdateRBACUserPassword(userID, hash); err != nil {
return err
}
a.mu.Lock()
for token, session := range a.sessions {
if session.UserID == userID {
delete(a.sessions, token)
}
}
a.mu.Unlock()
return nil
}
// RevokeToken invalidates the specified token.
@@ -106,6 +229,26 @@ func (a *AuthManager) RevokeToken(token string) {
a.mu.Unlock()
}
func (a *AuthManager) RevokeUserSessions(userID string) {
userID = strings.TrimSpace(userID)
if userID == "" {
return
}
a.mu.Lock()
for token, session := range a.sessions {
if session.UserID == userID {
delete(a.sessions, token)
}
}
a.mu.Unlock()
}
func (a *AuthManager) RevokeAllSessions() {
a.mu.Lock()
a.sessions = make(map[string]Session)
a.mu.Unlock()
}
// SessionDurationHours returns the configured session duration in hours.
func (a *AuthManager) SessionDurationHours() int {
return int(a.sessionDuration / time.Hour)
@@ -122,11 +265,34 @@ func (a *AuthManager) UpdateConfig(password string, sessionDurationHours int) er
sessionDurationHours = 12
}
a.mu.Lock()
defer a.mu.Unlock()
hash := ""
if a.db != nil {
var err error
hash, err = HashPassword(password)
if err != nil {
return err
}
}
a.mu.Lock()
a.password = password
a.sessionDuration = time.Duration(sessionDurationHours) * time.Hour
a.sessions = make(map[string]Session)
db := a.db
a.mu.Unlock()
if db != nil {
if err := db.UpdateRBACAdminPassword(hash); err != nil {
return err
}
}
return nil
}
func allPermissions() map[string]bool {
out := make(map[string]bool, len(PermissionCatalog))
for key := range PermissionCatalog {
out[key] = true
}
return out
}
+53
View File
@@ -0,0 +1,53 @@
package security
import (
"path/filepath"
"testing"
"cyberstrike-ai/internal/database"
"go.uber.org/zap"
)
func TestAuthManagerAuthenticatesCreatedRBACUser(t *testing.T) {
db, err := database.NewDB(filepath.Join(t.TempDir(), "auth-rbac.db"), zap.NewNop())
if err != nil {
t.Fatalf("NewDB: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
manager, err := NewAuthManager("admin-secret", 12)
if err != nil {
t.Fatalf("NewAuthManager: %v", err)
}
if err := manager.AttachRBACStore(db); err != nil {
t.Fatalf("AttachRBACStore: %v", err)
}
hash, err := HashPassword("operator-secret")
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
user, err := db.CreateRBACUser("operator1", "Operator One", hash, true, []string{database.RBACSystemRoleViewer})
if err != nil {
t.Fatalf("CreateRBACUser: %v", err)
}
token, _, err := manager.Authenticate("operator1", "operator-secret")
if err != nil {
t.Fatalf("Authenticate created user: %v", err)
}
session, ok := manager.ValidateToken(token)
if !ok {
t.Fatalf("expected created user session to validate")
}
if session.UserID != user.ID || session.Username != "operator1" {
t.Fatalf("session user = %s/%s, want %s/operator1", session.UserID, session.Username, user.ID)
}
if !session.Permissions["auth:self"] || !session.Permissions["chat:read"] {
t.Fatalf("expected viewer permissions in session, got %#v", session.Permissions)
}
if _, _, err := manager.Authenticate("", "operator-secret"); err == nil {
t.Fatalf("empty username must not authenticate non-admin user")
}
}
+90
View File
@@ -4,12 +4,18 @@ import (
"net/http"
"strings"
"cyberstrike-ai/internal/database"
"github.com/gin-gonic/gin"
)
const (
ContextAuthTokenKey = "authToken"
ContextSessionExpiry = "authSessionExpiry"
ContextUserIDKey = "authUserID"
ContextUsernameKey = "authUsername"
ContextUserScopeKey = "authUserScope"
ContextSessionKey = "authSession"
)
// AuthMiddleware enforces authentication on protected routes.
@@ -26,10 +32,94 @@ func AuthMiddleware(manager *AuthManager) gin.HandlerFunc {
c.Set(ContextAuthTokenKey, session.Token)
c.Set(ContextSessionExpiry, session.ExpiresAt)
c.Set(ContextUserIDKey, session.UserID)
c.Set(ContextUsernameKey, session.Username)
c.Set(ContextUserScopeKey, session.Scope)
c.Set(ContextSessionKey, session)
c.Next()
}
}
func RequirePermission(permission string) gin.HandlerFunc {
permission = strings.TrimSpace(permission)
return func(c *gin.Context) {
if permission == "" || SessionHasPermission(c, permission) {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "权限不足",
"permission": permission,
})
}
}
func RequireAnyPermission(permissions ...string) gin.HandlerFunc {
return func(c *gin.Context) {
for _, permission := range permissions {
if SessionHasPermission(c, permission) {
c.Next()
return
}
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "权限不足",
"permissions": permissions,
})
}
}
func RequireResourcePermission(db *database.DB, permission, resourceType, paramName string) gin.HandlerFunc {
return func(c *gin.Context) {
if !SessionHasPermission(c, permission) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "权限不足",
"permission": permission,
})
return
}
if db == nil {
c.Next()
return
}
resourceID := strings.TrimSpace(c.Param(paramName))
if resourceID == "" {
c.Next()
return
}
session, ok := CurrentSession(c)
if !ok || !db.UserCanAccessResource(session.UserID, session.Scope, resourceType, resourceID) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "无权访问该资源",
"resource_type": resourceType,
"resource_id": resourceID,
})
return
}
c.Next()
}
}
func CurrentSession(c *gin.Context) (Session, bool) {
if c == nil {
return Session{}, false
}
v, ok := c.Get(ContextSessionKey)
if !ok {
return Session{}, false
}
session, ok := v.(Session)
return session, ok
}
func SessionHasPermission(c *gin.Context, permission string) bool {
session, ok := CurrentSession(c)
if !ok {
return false
}
return session.Permissions[permission]
}
func extractTokenFromRequest(c *gin.Context) string {
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
+112
View File
@@ -0,0 +1,112 @@
package security
import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
)
// Platform permissions use module:action naming. They are intentionally
// separate from AI testing roles under roles/.
var PermissionCatalog = map[string]string{
"auth:self": "Manage own session and password",
"dashboard:read": "View dashboard summaries",
"chat:read": "View conversations",
"chat:write": "Create and update conversations",
"chat:delete": "Delete conversations and turns",
"agent:execute": "Run AI agents and workflows",
"hitl:read": "View HITL queues and logs",
"hitl:write": "Approve, dismiss, and configure HITL",
"tasks:read": "View task queues",
"tasks:write": "Create and run task queues",
"tasks:delete": "Delete task queues",
"project:read": "View projects and project facts",
"project:write": "Create and update projects and facts",
"project:delete": "Delete projects and facts",
"vulnerability:read": "View vulnerabilities",
"vulnerability:write": "Create and update vulnerabilities",
"vulnerability:delete": "Delete vulnerabilities",
"webshell:read": "View WebShell connections",
"webshell:write": "Manage and use WebShell connections",
"webshell:delete": "Delete WebShell connections",
"c2:read": "View C2 listeners, sessions, tasks, events, and profiles",
"c2:write": "Operate C2 listeners, sessions, tasks, payloads, files, and profiles",
"c2:delete": "Delete C2 objects",
"mcp:read": "View MCP status and external MCP configuration",
"mcp:write": "Manage external MCP servers and invoke MCP endpoint",
"knowledge:read": "View knowledge base and retrieval logs",
"knowledge:write": "Create, update, index, and scan knowledge base",
"knowledge:delete": "Delete knowledge items and retrieval logs",
"skills:read": "View skills and skill stats",
"skills:write": "Create and update skills",
"skills:delete": "Delete skills and stats",
"agents:read": "View markdown agents",
"agents:write": "Create and update markdown agents",
"agents:delete": "Delete markdown agents",
"roles:read": "View AI testing roles",
"roles:write": "Create and update AI testing roles",
"roles:delete": "Delete AI testing roles",
"workflow:read": "View workflow definitions and runs",
"workflow:write": "Create, update, validate, run, and resume workflows",
"workflow:delete": "Delete workflows",
"config:read": "View system configuration",
"config:write": "Update and apply system configuration",
"terminal:execute": "Run terminal commands",
"audit:read": "View and export audit logs",
"audit:delete": "Delete audit logs",
"rbac:read": "View users, platform roles, permissions, and assignments",
"rbac:write": "Manage users, platform roles, permissions, and assignments",
"notification:read": "View notifications",
"notification:write": "Mark notifications as read",
"robot:read": "View robot binding status",
"robot:write": "Manage robot bindings and test robot callbacks",
"files:read": "View chat uploads",
"files:write": "Upload, edit, and rename chat files",
"files:delete": "Delete chat files",
"attackchain:read": "View attack chains",
"attackchain:write": "Regenerate attack chains",
"fofa:execute": "Run FOFA searches and query parsing",
"openapi:read": "Read OpenAPI aggregation results",
"group:read": "View conversation groups",
"group:write": "Create and update conversation groups",
"group:delete": "Delete conversation groups",
"monitor:read": "View execution monitor",
"monitor:write": "Cancel monitor executions",
"monitor:delete": "Delete monitor executions",
}
func HashPassword(password string) (string, error) {
password = strings.TrimSpace(password)
if password == "" {
return "", fmt.Errorf("password is empty")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func VerifyPasswordHash(password, encoded string) bool {
if strings.HasPrefix(encoded, "$2a$") || strings.HasPrefix(encoded, "$2b$") || strings.HasPrefix(encoded, "$2y$") {
return bcrypt.CompareHashAndPassword([]byte(encoded), []byte(strings.TrimSpace(password))) == nil
}
parts := strings.Split(encoded, "$")
if len(parts) != 3 || parts[0] != "sha256" {
return false
}
salt, err := hex.DecodeString(parts[1])
if err != nil {
return false
}
expected, err := hex.DecodeString(parts[2])
if err != nil {
return false
}
sum := sha256.Sum256(append(salt, []byte(strings.TrimSpace(password))...))
return subtle.ConstantTimeCompare(sum[:], expected) == 1
}
+156
View File
@@ -0,0 +1,156 @@
package security
import (
"net/http"
"strings"
"cyberstrike-ai/internal/database"
"github.com/gin-gonic/gin"
)
// RBACMiddleware maps protected API routes to platform permissions. It keeps
// enforcement centralized so route declarations stay readable.
func RBACMiddleware(db *database.DB) gin.HandlerFunc {
return func(c *gin.Context) {
permission := permissionForRequest(c.Request.Method, c.FullPath())
if permission == "" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "未配置访问权限",
})
return
}
if SessionHasPermission(c, permission) {
if db != nil && !resourceAllowed(c, db) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
return
}
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "权限不足",
"permission": permission,
})
}
}
func permissionForRequest(method, fullPath string) string {
path := strings.TrimPrefix(fullPath, "/api")
switch {
case path == "/rbac/me":
return "auth:self"
case path == "/rbac/resources":
// The picker enumerates resource names and IDs and is only needed by
// administrators who can actually create assignments.
return "rbac:write"
case strings.HasPrefix(path, "/rbac"):
if method == http.MethodGet {
return "rbac:read"
}
return "rbac:write"
case strings.HasPrefix(path, "/robot/wechat/status"):
return "robot:read"
case strings.HasPrefix(path, "/robot"):
return "robot:write"
case strings.HasPrefix(path, "/eino-agent"), strings.HasPrefix(path, "/multi-agent"):
if strings.Contains(path, "/markdown-agents") {
return crudPermission(method, "agents")
}
return "agent:execute"
case strings.HasPrefix(path, "/hitl"):
return crudPermission(method, "hitl")
case strings.HasPrefix(path, "/agent-loop"), strings.HasPrefix(path, "/batch-tasks"):
return crudPermission(method, "tasks")
case strings.HasPrefix(path, "/conversations"), strings.HasPrefix(path, "/messages"), strings.HasPrefix(path, "/process-details"):
return crudPermission(method, "chat")
case strings.HasPrefix(path, "/groups"):
return crudPermission(method, "group")
case strings.HasPrefix(path, "/monitor"):
return crudPermission(method, "monitor")
case strings.HasPrefix(path, "/notifications"):
if method == http.MethodGet {
return "notification:read"
}
return "notification:write"
case strings.HasPrefix(path, "/config"):
return crudPermission(method, "config")
case strings.HasPrefix(path, "/terminal"):
return "terminal:execute"
case strings.HasPrefix(path, "/audit"):
return crudPermission(method, "audit")
case strings.HasPrefix(path, "/external-mcp"), path == "/mcp":
return crudPermission(method, "mcp")
case strings.HasPrefix(path, "/attack-chain"):
return crudPermission(method, "attackchain")
case strings.HasPrefix(path, "/knowledge"):
return crudPermission(method, "knowledge")
case strings.HasPrefix(path, "/vulnerabilities"):
return crudPermission(method, "vulnerability")
case strings.HasPrefix(path, "/projects"):
return crudPermission(method, "project")
case strings.HasPrefix(path, "/webshell"):
return crudPermission(method, "webshell")
case strings.HasPrefix(path, "/c2"):
return crudPermission(method, "c2")
case strings.HasPrefix(path, "/chat-uploads"):
return crudPermission(method, "files")
case strings.HasPrefix(path, "/roles"):
return crudPermission(method, "roles")
case strings.HasPrefix(path, "/workflows"):
return crudPermission(method, "workflow")
case strings.HasPrefix(path, "/skills"):
return crudPermission(method, "skills")
case strings.HasPrefix(path, "/openapi"):
return "openapi:read"
case strings.HasPrefix(path, "/fofa"):
return "fofa:execute"
default:
return ""
}
}
func crudPermission(method, module string) string {
switch method {
case http.MethodGet, http.MethodHead:
return module + ":read"
case http.MethodDelete:
return module + ":delete"
default:
return module + ":write"
}
}
func resourceAllowed(c *gin.Context, db *database.DB) bool {
session, ok := CurrentSession(c)
if !ok || session.Scope == database.RBACScopeAll {
return ok
}
path := strings.TrimPrefix(c.FullPath(), "/api")
switch {
case strings.HasPrefix(path, "/projects/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "project", c.Param("id"))
case strings.HasPrefix(path, "/conversations/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "conversation", c.Param("id"))
case strings.HasPrefix(path, "/messages/:id/process-details"):
return db.UserCanAccessMessage(session.UserID, session.Scope, c.Param("id"))
case strings.HasPrefix(path, "/process-details/:id"):
return db.UserCanAccessProcessDetail(session.UserID, session.Scope, c.Param("id"))
case strings.HasPrefix(path, "/attack-chain/:conversationId"):
return db.UserCanAccessResource(session.UserID, session.Scope, "conversation", c.Param("conversationId"))
case strings.HasPrefix(path, "/webshell/connections/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "webshell", c.Param("id"))
case strings.HasPrefix(path, "/batch-tasks/:queueId"):
return db.UserCanAccessResource(session.UserID, session.Scope, "batch_task", c.Param("queueId"))
case strings.HasPrefix(path, "/vulnerabilities/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "vulnerability", c.Param("id"))
case strings.HasPrefix(path, "/c2/listeners/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "c2_listener", c.Param("id"))
case strings.HasPrefix(path, "/c2/sessions/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "c2_session", c.Param("id"))
case strings.HasPrefix(path, "/c2/tasks/:id"):
return db.UserCanAccessResource(session.UserID, session.Scope, "c2_task", c.Param("id"))
default:
return true
}
}
+120
View File
@@ -0,0 +1,120 @@
package security
import (
"net/http"
"net/http/httptest"
"testing"
"cyberstrike-ai/internal/database"
"github.com/gin-gonic/gin"
)
func TestRBACMiddlewareUsesMatchedFullPath(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(ContextSessionKey, Session{
UserID: "u1",
Username: "operator",
Permissions: map[string]bool{"project:read": true},
Scope: database.RBACScopeAll,
})
c.Next()
})
router.Use(RBACMiddleware(nil))
router.GET("/api/projects/:id", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/projects/p1", nil)
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d: %s", w.Code, http.StatusOK, w.Body.String())
}
}
func TestRBACMiddlewareRejectsMissingPermission(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(ContextSessionKey, Session{
UserID: "u1",
Username: "viewer",
Permissions: map[string]bool{"project:read": true},
Scope: database.RBACScopeAll,
})
c.Next()
})
router.Use(RBACMiddleware(nil))
router.POST("/api/projects", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/projects", nil)
router.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", w.Code, http.StatusForbidden)
}
}
func TestRBACMiddlewareRejectsUnmappedProtectedRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(ContextSessionKey, Session{
UserID: "u1",
Username: "admin",
Permissions: allPermissions(),
Scope: database.RBACScopeAll,
})
c.Next()
})
router.Use(RBACMiddleware(nil))
router.GET("/api/new-module", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/new-module", nil)
router.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", w.Code, http.StatusForbidden)
}
}
func TestRBACMiddlewareMapsOpenAPISpec(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(ContextSessionKey, Session{
UserID: "u1",
Username: "viewer",
Permissions: map[string]bool{"openapi:read": true},
Scope: database.RBACScopeAll,
})
c.Next()
})
router.Use(RBACMiddleware(nil))
router.GET("/api/openapi/spec", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/openapi/spec", nil)
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d: %s", w.Code, http.StatusOK, w.Body.String())
}
}
func TestRBACResourcePickerRequiresWritePermission(t *testing.T) {
if got := permissionForRequest(http.MethodGet, "/api/rbac/resources"); got != "rbac:write" {
t.Fatalf("picker permission = %q, want rbac:write", got)
}
if got := permissionForRequest(http.MethodGet, "/api/rbac/resource-assignments"); got != "rbac:read" {
t.Fatalf("assignment list permission = %q, want rbac:read", got)
}
}