mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-13 23:47:24 +02:00
308 lines
7.1 KiB
Go
308 lines
7.1 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 {
|
|
password string
|
|
sessionDuration time.Duration
|
|
db *database.DB
|
|
|
|
mu sync.RWMutex
|
|
sessions map[string]Session
|
|
}
|
|
|
|
// NewAuthManager creates a new AuthManager instance.
|
|
func NewAuthManager(password string, sessionDurationHours int) (*AuthManager, error) {
|
|
if strings.TrimSpace(password) == "" {
|
|
return nil, errors.New("auth password must be configured")
|
|
}
|
|
|
|
if sessionDurationHours <= 0 {
|
|
sessionDurationHours = 12
|
|
}
|
|
|
|
return &AuthManager{
|
|
password: password,
|
|
sessionDuration: time.Duration(sessionDurationHours) * time.Hour,
|
|
sessions: make(map[string]Session),
|
|
}, nil
|
|
}
|
|
|
|
// 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.RLock()
|
|
db := a.db
|
|
legacyPassword := a.password
|
|
a.mu.RUnlock()
|
|
|
|
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,
|
|
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
|
|
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.
|
|
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)
|
|
}
|
|
|
|
// UpdateConfig updates the password and session duration, revoking existing sessions.
|
|
func (a *AuthManager) UpdateConfig(password string, sessionDurationHours int) error {
|
|
password = strings.TrimSpace(password)
|
|
if password == "" {
|
|
return errors.New("auth password must be configured")
|
|
}
|
|
|
|
if sessionDurationHours <= 0 {
|
|
sessionDurationHours = 12
|
|
}
|
|
|
|
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
|
|
}
|