refactor(auth): add tenant scoping and permission enhancements

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
This commit is contained in:
Jeremie Fraeys 2026-02-26 12:06:08 -05:00
parent 420de879ff
commit ef11d88a75
No known key found for this signature in database
4 changed files with 24 additions and 22 deletions

View file

@ -15,10 +15,10 @@ import (
// User represents an authenticated user
type User struct {
Name string `json:"name"`
Admin bool `json:"admin"`
Roles []string `json:"roles"`
Permissions map[string]bool `json:"permissions"`
Name string `json:"name"`
Roles []string `json:"roles"`
Admin bool `json:"admin"`
}
// ExtractAPIKeyFromRequest extracts an API key from the standard headers.
@ -41,12 +41,12 @@ type APIKeyHash string
// APIKeyEntry represents an API key configuration
type APIKeyEntry struct {
Hash APIKeyHash `yaml:"hash"`
Salt string `yaml:"salt,omitempty"` // Salt for Argon2id hashing
Algorithm string `yaml:"algorithm,omitempty"` // "sha256" or "argon2id"
Admin bool `yaml:"admin"`
Roles []string `yaml:"roles,omitempty"`
Permissions map[string]bool `yaml:"permissions,omitempty"`
Hash APIKeyHash `yaml:"hash"`
Salt string `yaml:"salt,omitempty"`
Algorithm string `yaml:"algorithm,omitempty"`
Roles []string `yaml:"roles,omitempty"`
Admin bool `yaml:"admin"`
}
// Username represents a user identifier
@ -54,8 +54,8 @@ type Username string
// Config represents the authentication configuration
type Config struct {
Enabled bool `yaml:"enabled"`
APIKeys map[Username]APIKeyEntry `yaml:"api_keys"`
Enabled bool `yaml:"enabled"`
}
// Store interface for different authentication backends
@ -81,12 +81,12 @@ const userContextKey = contextKey("user")
// UserInfo represents user information from authentication store
type UserInfo struct {
UserID string `json:"user_id"`
Admin bool `json:"admin"`
KeyHash string `json:"key_hash"`
Created time.Time `json:"created"`
Expires *time.Time `json:"expires,omitempty"`
Revoked *time.Time `json:"revoked,omitempty"`
UserID string `json:"user_id"`
KeyHash string `json:"key_hash"`
Admin bool `json:"admin"`
}
// ValidateAPIKey validates an API key and returns user information

View file

@ -18,15 +18,15 @@ type DatabaseAuthStore struct {
// APIKeyRecord represents an API key in the database
type APIKeyRecord struct {
ID int `json:"id"`
UserID string `json:"user_id"`
KeyHash string `json:"key_hash"`
Admin bool `json:"admin"`
Roles string `json:"roles"` // JSON array
Permissions string `json:"permissions"` // JSON object
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

View file

@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/jfraeys/fetch_ml/internal/fileutil"
"github.com/zalando/go-keyring"
)
@ -95,7 +96,7 @@ func (km *KeychainManager) DeleteAPIKey(service, account string) error {
// Try to delete from primary keyring, but don't fail on keyring errors
// (e.g., dbus unavailable, permission denied) - just clean up fallback
_ = km.primary.Delete(service, account)
// Always clean up fallback
if err := km.fallback.delete(service, account); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
@ -136,7 +137,8 @@ func (f *fileKeyStore) store(service, account, secret string) error {
return fmt.Errorf("failed to prepare key store: %w", err)
}
path := f.path(service, account)
return os.WriteFile(path, []byte(secret), 0o600)
// SECURITY: Write with fsync for crash safety
return fileutil.WriteFileSafe(path, []byte(secret), 0o600)
}
func (f *fileKeyStore) get(service, account string) (string, error) {

View file

@ -47,8 +47,8 @@ const (
// PermissionGroup represents a group of related permissions
type PermissionGroup struct {
Name string
Permissions []string
Description string
Permissions []string
}
// PermissionGroups defines built-in permission groups.
@ -167,11 +167,11 @@ func ExpandPermissionGroups(groups []string) ([]string, error) {
// PermissionCheckResult represents the result of a permission check
type PermissionCheckResult struct {
Allowed bool `json:"allowed"`
Permission string `json:"permission"`
User string `json:"user"`
Roles []string `json:"roles"`
Missing []string `json:"missing,omitempty"`
Allowed bool `json:"allowed"`
}
// CheckMultiplePermissions checks multiple permissions at once