mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-20 13:30:51 +02:00
feat: Implement authentication provider management components
- Added new templates for managing authentication providers, including forms for creating and editing providers. - Implemented backend logic to handle retrieval, creation, and deletion of authentication providers. - Introduced new database migrations to support the storage of authentication provider data. - Enhanced the user interface to display available authentication providers and their statuses. - Added routes and handlers for managing authentication provider actions in the web application.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProviderType represents the type of authentication provider
|
||||
type ProviderType string
|
||||
|
||||
const (
|
||||
// ProviderTypeAuthentik represents an Authentik authentication provider
|
||||
ProviderTypeAuthentik ProviderType = "authentik"
|
||||
|
||||
// ProviderTypeOIDC represents an OpenID Connect authentication provider
|
||||
ProviderTypeOIDC ProviderType = "oidc"
|
||||
|
||||
// ProviderTypeSAML represents a SAML authentication provider
|
||||
ProviderTypeSAML ProviderType = "saml"
|
||||
|
||||
// ProviderTypeOAuth2 represents an OAuth2 authentication provider
|
||||
ProviderTypeOAuth2 ProviderType = "oauth2"
|
||||
)
|
||||
|
||||
// AuthProvider represents an external authentication provider configuration
|
||||
type AuthProvider struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Type ProviderType `gorm:"not null" json:"type"`
|
||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
ProviderURL string `json:"provider_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"-"` // Not returned in JSON responses
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Scopes string `json:"scopes"`
|
||||
AttributeMapping string `json:"attribute_mapping"`
|
||||
Config string `json:"-"` // Stores provider-specific configuration
|
||||
IconURL string `json:"icon_url"` // URL to provider icon
|
||||
SuccessfulLogins int `json:"successful_logins"`
|
||||
LastUsed sql.NullTime `json:"last_used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Unmarshalled config
|
||||
configData map[string]interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// GetConfig returns the unmarshalled configuration data
|
||||
func (p *AuthProvider) GetConfig() (map[string]interface{}, error) {
|
||||
if p.configData == nil && p.Config != "" {
|
||||
err := json.Unmarshal([]byte(p.Config), &p.configData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p.configData == nil {
|
||||
p.configData = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return p.configData, nil
|
||||
}
|
||||
|
||||
// SetConfig sets the configuration data and marshals it to JSON
|
||||
func (p *AuthProvider) SetConfig(data map[string]interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Config = string(jsonData)
|
||||
p.configData = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExternalUserIdentity represents a user identity from an external authentication provider
|
||||
type ExternalUserIdentity struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
ProviderID uint `gorm:"not null" json:"provider_id"`
|
||||
ProviderType ProviderType `gorm:"not null" json:"provider_type"`
|
||||
ExternalID string `gorm:"not null" json:"external_id"`
|
||||
Email string `gorm:"not null" json:"email"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Groups string `json:"groups"` // JSON array of groups
|
||||
LastLogin sql.NullTime `json:"last_login"`
|
||||
ProviderData string `json:"-"` // Raw data from provider
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Foreign key relationships
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
Provider AuthProvider `gorm:"foreignKey:ProviderID" json:"-"`
|
||||
|
||||
// Unmarshalled provider data
|
||||
providerDataObj map[string]interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// GetProviderData returns the unmarshalled provider data
|
||||
func (e *ExternalUserIdentity) GetProviderData() (map[string]interface{}, error) {
|
||||
if e.providerDataObj == nil && e.ProviderData != "" {
|
||||
err := json.Unmarshal([]byte(e.ProviderData), &e.providerDataObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if e.providerDataObj == nil {
|
||||
e.providerDataObj = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return e.providerDataObj, nil
|
||||
}
|
||||
|
||||
// SetProviderData sets the provider data and marshals it to JSON
|
||||
func (e *ExternalUserIdentity) SetProviderData(data map[string]interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.ProviderData = string(jsonData)
|
||||
e.providerDataObj = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGroups returns the unmarshalled groups
|
||||
func (e *ExternalUserIdentity) GetGroups() ([]string, error) {
|
||||
var groups []string
|
||||
if e.Groups == "" {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(e.Groups), &groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// SetGroups sets the groups and marshals them to JSON
|
||||
func (e *ExternalUserIdentity) SetGroups(groups []string) error {
|
||||
jsonData, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Groups = string(jsonData)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetAllAuthProviders returns all authentication providers
|
||||
func (db *DB) GetAllAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
var providers []AuthProvider
|
||||
tx := db.WithContext(ctx).Order("name asc").Find(&providers)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get auth providers: %w", tx.Error)
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// GetAuthProviderByID retrieves an authentication provider by ID
|
||||
func (db *DB) GetAuthProviderByID(ctx context.Context, id uint) (*AuthProvider, error) {
|
||||
var provider AuthProvider
|
||||
tx := db.WithContext(ctx).First(&provider, id)
|
||||
if tx.Error != nil {
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("auth provider not found: %d", id)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get auth provider: %w", tx.Error)
|
||||
}
|
||||
return &provider, nil
|
||||
}
|
||||
|
||||
// CreateAuthProvider creates a new authentication provider
|
||||
func (db *DB) CreateAuthProvider(ctx context.Context, provider *AuthProvider) error {
|
||||
tx := db.WithContext(ctx).Create(provider)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to create auth provider: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAuthProvider updates an existing authentication provider
|
||||
func (db *DB) UpdateAuthProvider(ctx context.Context, provider *AuthProvider) error {
|
||||
tx := db.WithContext(ctx).Save(provider)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to update auth provider: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAuthProvider deletes an authentication provider by ID
|
||||
func (db *DB) DeleteAuthProvider(ctx context.Context, id uint) error {
|
||||
tx := db.WithContext(ctx).Delete(&AuthProvider{}, id)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to delete auth provider: %w", tx.Error)
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
return fmt.Errorf("auth provider not found: %d", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEnabledAuthProviders returns all enabled authentication providers
|
||||
func (db *DB) GetEnabledAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
var providers []AuthProvider
|
||||
tx := db.WithContext(ctx).Where("enabled = ?", true).Order("name asc").Find(&providers)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get enabled auth providers: %w", tx.Error)
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// GetAuthProviderByType returns authentication providers of a specific type
|
||||
func (db *DB) GetAuthProviderByType(ctx context.Context, providerType ProviderType) ([]AuthProvider, error) {
|
||||
var providers []AuthProvider
|
||||
tx := db.WithContext(ctx).Where("type = ?", providerType).Order("name asc").Find(&providers)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get auth providers by type: %w", tx.Error)
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// GetExternalUserIdentitiesByProviderID returns all external user identities for a specific provider
|
||||
func (db *DB) GetExternalUserIdentitiesByProviderID(ctx context.Context, providerID uint) ([]ExternalUserIdentity, error) {
|
||||
var identities []ExternalUserIdentity
|
||||
tx := db.WithContext(ctx).Where("provider_id = ?", providerID).Find(&identities)
|
||||
if tx.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get external user identities: %w", tx.Error)
|
||||
}
|
||||
return identities, nil
|
||||
}
|
||||
|
||||
// CountExternalUserIdentitiesByProviderID counts the number of external user identities for a specific provider
|
||||
func (db *DB) CountExternalUserIdentitiesByProviderID(ctx context.Context, providerID uint) (int64, error) {
|
||||
var count int64
|
||||
tx := db.WithContext(ctx).Model(&ExternalUserIdentity{}).Where("provider_id = ?", providerID).Count(&count)
|
||||
if tx.Error != nil {
|
||||
return 0, fmt.Errorf("failed to count external user identities: %w", tx.Error)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetExternalUserIdentity gets an external user identity by provider ID and external ID
|
||||
func (db *DB) GetExternalUserIdentity(ctx context.Context, providerID uint, externalID string) (*ExternalUserIdentity, error) {
|
||||
var identity ExternalUserIdentity
|
||||
tx := db.WithContext(ctx).Where("provider_id = ? AND external_id = ?", providerID, externalID).First(&identity)
|
||||
if tx.Error != nil {
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // Not found, but not an error
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get external user identity: %w", tx.Error)
|
||||
}
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
// CreateExternalUserIdentity creates a new external user identity
|
||||
func (db *DB) CreateExternalUserIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
tx := db.WithContext(ctx).Create(identity)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to create external user identity: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateExternalUserIdentity updates an existing external user identity
|
||||
func (db *DB) UpdateExternalUserIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
tx := db.WithContext(ctx).Save(identity)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to update external user identity: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExternalUserIdentity deletes an external user identity by ID
|
||||
func (db *DB) DeleteExternalUserIdentity(ctx context.Context, id uint) error {
|
||||
tx := db.WithContext(ctx).Delete(&ExternalUserIdentity{}, id)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to delete external user identity: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAuthProviderLastUsed updates the last used timestamp and increments the successful logins counter
|
||||
func (db *DB) UpdateAuthProviderLastUsed(ctx context.Context, providerID uint) error {
|
||||
tx := db.WithContext(ctx).Model(&AuthProvider{}).
|
||||
Where("id = ?", providerID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_used": gorm.Expr("NOW()"),
|
||||
"successful_logins": gorm.Expr("successful_logins + 1"),
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("failed to update auth provider last used: %w", tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -2079,3 +2080,30 @@ func (db *DB) GetRcloneCommandFlagsMap(commandID uint) (map[uint]RcloneCommandFl
|
||||
|
||||
return flagsMap, nil
|
||||
}
|
||||
|
||||
// GetEnabledAuthProviders returns all enabled authentication providers
|
||||
// func (db *DB) GetEnabledAuthProviders(ctx context.Context) ([]AuthProvider, error) {
|
||||
// var providers []AuthProvider
|
||||
// result := db.WithContext(ctx).Where("enabled = ?", true).Find(&providers)
|
||||
// return providers, result.Error
|
||||
// }
|
||||
|
||||
// GetExternalIdentity retrieves an external identity by provider ID and external ID
|
||||
func (db *DB) GetExternalIdentity(ctx context.Context, providerID uint, externalID string) (*ExternalUserIdentity, error) {
|
||||
var identity ExternalUserIdentity
|
||||
result := db.WithContext(ctx).Where("provider_id = ? AND external_id = ?", providerID, externalID).First(&identity)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
// CreateExternalIdentity creates a new external user identity
|
||||
func (db *DB) CreateExternalIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
return db.WithContext(ctx).Create(identity).Error
|
||||
}
|
||||
|
||||
// UpdateExternalIdentity updates an existing external user identity
|
||||
func (db *DB) UpdateExternalIdentity(ctx context.Context, identity *ExternalUserIdentity) error {
|
||||
return db.WithContext(ctx).Save(identity).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AddAuthProviders adds tables for external authentication providers
|
||||
func AddAuthProviders() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "011_add_auth_providers",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Check if any tables exist (indicating an existing database)
|
||||
var count int64
|
||||
if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check for existing tables: %v", err)
|
||||
}
|
||||
|
||||
// If tables exist, create a backup
|
||||
if count > 0 {
|
||||
// Get the database path
|
||||
sqlDB, err := tx.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying database: %v", err)
|
||||
}
|
||||
|
||||
var seq int
|
||||
var name, dbPath string
|
||||
if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
|
||||
return fmt.Errorf("failed to get database path: %v", err)
|
||||
}
|
||||
|
||||
// Get backup directory from environment variable or use default
|
||||
backupDir := os.Getenv("BACKUP_DIR")
|
||||
if backupDir == "" {
|
||||
backupDir = "/app/backups" // Default Docker path
|
||||
// Check if we're not in Docker
|
||||
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
||||
backupDir = "backups" // Fallback to local directory
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup directory if it doesn't exist
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %v", err)
|
||||
}
|
||||
|
||||
// Create backup file with timestamp in the backup directory
|
||||
dbFileName := filepath.Base(dbPath)
|
||||
backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
|
||||
backupFile := filepath.Join(backupDir, backupFileName)
|
||||
|
||||
// Read original database
|
||||
data, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read database for backup: %v", err)
|
||||
}
|
||||
|
||||
// Write backup
|
||||
if err := os.WriteFile(backupFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write database backup: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created database backup at %s\n", backupFile)
|
||||
}
|
||||
|
||||
// Create auth_providers table
|
||||
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS auth_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
description TEXT,
|
||||
provider_url TEXT,
|
||||
icon_url TEXT,
|
||||
client_id VARCHAR(255),
|
||||
client_secret VARCHAR(255),
|
||||
redirect_url TEXT,
|
||||
scopes TEXT,
|
||||
attribute_mapping TEXT,
|
||||
config TEXT,
|
||||
successful_logins INTEGER DEFAULT 0,
|
||||
last_used DATETIME,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create external_user_identities table
|
||||
if err := tx.Exec(`CREATE TABLE IF NOT EXISTS external_user_identities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(50) NOT NULL,
|
||||
external_id VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(255),
|
||||
display_name VARCHAR(255),
|
||||
groups TEXT,
|
||||
last_login DATETIME,
|
||||
provider_data TEXT,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE
|
||||
)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create unique index on provider_id and external_id
|
||||
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_external_user_identities_provider_external
|
||||
ON external_user_identities(provider_id, external_id)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
if err := tx.Exec("DROP TABLE IF EXISTS external_user_identities").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec("DROP TABLE IF EXISTS auth_providers").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ func GetMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
AddUserNotifications(), // 008
|
||||
AddRcloneTables(), // 009
|
||||
AddRcloneCommandToConfig(), // 010
|
||||
AddAuthProviders(), // 011
|
||||
)
|
||||
|
||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||
|
||||
Reference in New Issue
Block a user