Files
CyberStrikeAI/internal/security/auth_manager.go
T
2026-07-11 11:44:25 +08:00

267 lines
6.3 KiB
Go

package security
import (
"database/sql"
"errors"
"strings"
"sync"
"time"
"cyberstrike-ai/internal/database"
"github.com/google/uuid"
)
// Predefined errors for authentication operations.
var (
ErrInvalidPassword = errors.New("invalid password")
)
// Session represents an authenticated user session.
type Session struct {
Token string
ExpiresAt time.Time
UserID string
Username string
DisplayName string
Roles []string
Permissions map[string]bool
PermissionScopes map[string]string
Scope string
}
// AuthManager manages password-based authentication and session lifecycle.
type AuthManager struct {
sessionDuration time.Duration
db *database.DB
mu sync.RWMutex
sessions map[string]Session
}
// NewAuthManager creates a new AuthManager instance.
func NewAuthManager(sessionDurationHours int) *AuthManager {
if sessionDurationHours <= 0 {
sessionDurationHours = 12
}
return &AuthManager{
sessionDuration: time.Duration(sessionDurationHours) * time.Hour,
sessions: make(map[string]Session),
}
}
// AttachRBACStore enables multi-user RBAC authentication. When no users exist yet,
// it bootstraps the built-in admin account and returns the generated initial password.
func (a *AuthManager) AttachRBACStore(db *database.DB) (generatedAdminPassword string, err error) {
if db == nil {
return "", errors.New("database is required for authentication")
}
needsAdminPassword, err := db.RBACNeedsAdminPassword()
if err != nil {
return "", err
}
adminPasswordHash := ""
if needsAdminPassword {
generatedAdminPassword, err = GenerateStrongPassword(24)
if err != nil {
return "", err
}
adminPasswordHash, err = HashPassword(generatedAdminPassword)
if err != nil {
return "", err
}
}
if err := db.BootstrapRBAC(adminPasswordHash, PermissionCatalog); err != nil {
return "", err
}
a.mu.Lock()
a.db = db
a.mu.Unlock()
return generatedAdminPassword, 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.RLock()
db := a.db
a.mu.RUnlock()
if db == nil {
return Session{}, errors.New("authentication store is not configured")
}
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,
PermissionScopes: access.PermissionScopes,
Scope: access.Scope,
}, nil
}
func (s Session) ScopeFor(permission string) string {
if scope := strings.TrimSpace(s.PermissionScopes[strings.TrimSpace(permission)]); scope != "" {
return scope
}
return strings.TrimSpace(s.Scope)
}
// ValidateToken checks whether the provided token is still valid.
func (a *AuthManager) ValidateToken(token string) (Session, bool) {
if strings.TrimSpace(token) == "" {
return Session{}, false
}
a.mu.RLock()
session, ok := a.sessions[token]
a.mu.RUnlock()
if !ok {
return Session{}, false
}
if time.Now().After(session.ExpiresAt) {
a.mu.Lock()
delete(a.sessions, token)
a.mu.Unlock()
return Session{}, false
}
return session, true
}
// 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()
db := a.db
a.mu.RUnlock()
if db == nil {
return false
}
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 errors.New("authentication store is not configured")
}
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.
func (a *AuthManager) RevokeToken(token string) {
if strings.TrimSpace(token) == "" {
return
}
a.mu.Lock()
delete(a.sessions, token)
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)
}
func allPermissions() map[string]bool {
out := make(map[string]bool, len(PermissionCatalog))
for key := range PermissionCatalog {
out[key] = true
}
return out
}