Update authentication system for multi-tenant support: - API key management with tenant scoping - Permission checks for multi-tenant operations - Database layer with tenant isolation - Keychain integration with audit logging
235 lines
6 KiB
Go
235 lines
6 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
|
)
|
|
|
|
// DatabaseAuthStore implements authentication using SQLite database
|
|
type DatabaseAuthStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// APIKeyRecord represents an API key in the database
|
|
type APIKeyRecord struct {
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
|
UserID string `json:"user_id"`
|
|
KeyHash string `json:"key_hash"`
|
|
Roles string `json:"roles"`
|
|
Permissions string `json:"permissions"`
|
|
ID int `json:"id"`
|
|
Admin bool `json:"admin"`
|
|
}
|
|
|
|
// NewDatabaseAuthStore creates a new database-backed auth store
|
|
func NewDatabaseAuthStore(dbPath string) (*DatabaseAuthStore, error) {
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
store := &DatabaseAuthStore{db: db}
|
|
if err := store.init(); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
|
}
|
|
|
|
return store, nil
|
|
}
|
|
|
|
// init creates the necessary database tables
|
|
func (s *DatabaseAuthStore) init() error {
|
|
ctx := context.Background()
|
|
query := `
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL UNIQUE,
|
|
key_hash TEXT NOT NULL UNIQUE,
|
|
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
roles TEXT NOT NULL DEFAULT '[]',
|
|
permissions TEXT NOT NULL DEFAULT '{}',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at DATETIME,
|
|
revoked_at DATETIME,
|
|
CHECK (json_valid(roles)),
|
|
CHECK (json_valid(permissions))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(
|
|
revoked_at,
|
|
COALESCE(expires_at, '9999-12-31')
|
|
);
|
|
`
|
|
|
|
_, err := s.db.ExecContext(ctx, query)
|
|
return err
|
|
}
|
|
|
|
// ValidateAPIKey checks if an API key is valid and returns user info
|
|
func (s *DatabaseAuthStore) ValidateAPIKey(ctx context.Context, key string) (*User, error) {
|
|
keyHash := HashAPIKey(key)
|
|
|
|
query := `
|
|
SELECT user_id, admin, roles, permissions, expires_at, revoked_at
|
|
FROM api_keys
|
|
WHERE key_hash = ?
|
|
AND (revoked_at IS NULL OR revoked_at > ?)
|
|
AND (expires_at IS NULL OR expires_at > ?)
|
|
`
|
|
|
|
var userID string
|
|
var admin bool
|
|
var rolesJSON, permissionsJSON string
|
|
var expiresAt, revokedAt sql.NullTime
|
|
now := time.Now()
|
|
|
|
err := s.db.QueryRowContext(ctx, query, keyHash, now, now).Scan(
|
|
&userID, &admin, &rolesJSON, &permissionsJSON, &expiresAt, &revokedAt,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("invalid API key")
|
|
}
|
|
return nil, fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
// Parse roles
|
|
var roles []string
|
|
if err := json.Unmarshal([]byte(rolesJSON), &roles); err != nil {
|
|
log.Printf("Failed to parse roles for user %s: %v", userID, err)
|
|
roles = []string{}
|
|
}
|
|
|
|
// Parse permissions
|
|
var permissions map[string]bool
|
|
if err := json.Unmarshal([]byte(permissionsJSON), &permissions); err != nil {
|
|
log.Printf("Failed to parse permissions for user %s: %v", userID, err)
|
|
permissions = make(map[string]bool)
|
|
}
|
|
|
|
// Admin gets all permissions
|
|
if admin {
|
|
permissions["*"] = true
|
|
}
|
|
|
|
return &User{
|
|
Name: userID,
|
|
Admin: admin,
|
|
Roles: roles,
|
|
Permissions: permissions,
|
|
}, nil
|
|
}
|
|
|
|
// CreateAPIKey creates a new API key in the database
|
|
func (s *DatabaseAuthStore) CreateAPIKey(
|
|
ctx context.Context,
|
|
userID string,
|
|
keyHash string,
|
|
admin bool,
|
|
roles []string,
|
|
permissions map[string]bool,
|
|
expiresAt *time.Time,
|
|
) error {
|
|
rolesJSON, err := json.Marshal(roles)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal roles: %w", err)
|
|
}
|
|
|
|
permissionsJSON, err := json.Marshal(permissions)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal permissions: %w", err)
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO api_keys (user_id, key_hash, admin, roles, permissions, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
key_hash = excluded.key_hash,
|
|
admin = excluded.admin,
|
|
roles = excluded.roles,
|
|
permissions = excluded.permissions,
|
|
expires_at = excluded.expires_at,
|
|
revoked_at = NULL
|
|
`
|
|
|
|
_, err = s.db.ExecContext(
|
|
ctx,
|
|
query,
|
|
userID,
|
|
keyHash,
|
|
admin,
|
|
rolesJSON,
|
|
permissionsJSON,
|
|
expiresAt,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// RevokeAPIKey revokes an API key
|
|
func (s *DatabaseAuthStore) RevokeAPIKey(ctx context.Context, userID string) error {
|
|
query := `UPDATE api_keys SET revoked_at = CURRENT_TIMESTAMP WHERE user_id = ?`
|
|
_, err := s.db.ExecContext(ctx, query, userID)
|
|
return err
|
|
}
|
|
|
|
// ListUsers returns all active users
|
|
func (s *DatabaseAuthStore) ListUsers(ctx context.Context) ([]APIKeyRecord, error) {
|
|
query := `
|
|
SELECT id, user_id, key_hash, admin, roles, permissions, created_at, expires_at, revoked_at
|
|
FROM api_keys
|
|
WHERE revoked_at IS NULL
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query users: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var users []APIKeyRecord
|
|
for rows.Next() {
|
|
var user APIKeyRecord
|
|
err := rows.Scan(
|
|
&user.ID, &user.UserID, &user.KeyHash, &user.Admin,
|
|
&user.Roles, &user.Permissions, &user.CreatedAt,
|
|
&user.ExpiresAt, &user.RevokedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan user: %w", err)
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating users: %w", err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// CleanupExpiredKeys removes expired and revoked keys
|
|
func (s *DatabaseAuthStore) CleanupExpiredKeys(ctx context.Context) error {
|
|
query := `
|
|
DELETE FROM api_keys
|
|
WHERE (revoked_at IS NOT NULL AND revoked_at < datetime('now', '-30 days'))
|
|
OR (expires_at IS NOT NULL AND expires_at < datetime('now', '-7 days'))
|
|
`
|
|
|
|
_, err := s.db.ExecContext(ctx, query)
|
|
return err
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (s *DatabaseAuthStore) Close() error {
|
|
return s.db.Close()
|
|
}
|