From 746aa71d67299bf82685ff50696a82bebce8b409 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sun, 11 Jan 2026 15:36:27 +0100 Subject: [PATCH] feat: add static api key env variable (#1229) --- .../internal/bootstrap/services_bootstrap.go | 7 +- backend/internal/common/env_config.go | 5 ++ backend/internal/middleware/api_key_auth.go | 2 +- backend/internal/service/api_key_service.go | 64 ++++++++++++++++++- .../internal/service/user_signup_service.go | 4 +- .../settings/admin/users/user-list.svelte | 2 +- 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/backend/internal/bootstrap/services_bootstrap.go b/backend/internal/bootstrap/services_bootstrap.go index 4cb6c86d..069b814c 100644 --- a/backend/internal/bootstrap/services_bootstrap.go +++ b/backend/internal/bootstrap/services_bootstrap.go @@ -75,7 +75,12 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService, svc.scimService) svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, svc.scimService, fileStorage) svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService, fileStorage) - svc.apiKeyService = service.NewApiKeyService(db, svc.emailService) + + svc.apiKeyService, err = service.NewApiKeyService(ctx, db, svc.emailService) + if err != nil { + return nil, fmt.Errorf("failed to create API key service: %w", err) + } + svc.userSignUpService = service.NewUserSignupService(db, svc.jwtService, svc.auditLogService, svc.appConfigService, svc.userService) svc.oneTimeAccessService = service.NewOneTimeAccessService(db, svc.userService, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 98b11497..945b4b1b 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -50,6 +50,7 @@ type EnvConfigSchema struct { InternalAppURL string `env:"INTERNAL_APP_URL"` UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` DisableRateLimiting bool `env:"DISABLE_RATE_LIMITING"` + StaticApiKey string `env:"STATIC_API_KEY" options:"file"` FileBackend string `env:"FILE_BACKEND" options:"toLower"` UploadPath string `env:"UPLOAD_PATH"` @@ -200,6 +201,10 @@ func ValidateEnvConfig(config *EnvConfigSchema) error { return errors.New("AUDIT_LOG_RETENTION_DAYS must be greater than 0") } + if config.StaticApiKey != "" && len(config.StaticApiKey) < 16 { + return errors.New("STATIC_API_KEY must be at least 16 characters long") + } + return nil } diff --git a/backend/internal/middleware/api_key_auth.go b/backend/internal/middleware/api_key_auth.go index fa35dd6f..34dbcbfd 100644 --- a/backend/internal/middleware/api_key_auth.go +++ b/backend/internal/middleware/api_key_auth.go @@ -34,7 +34,7 @@ func (m *ApiKeyAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc { } func (m *ApiKeyAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (userID string, isAdmin bool, err error) { - apiKey := c.GetHeader("X-API-KEY") + apiKey := c.GetHeader("X-API-Key") user, err := m.apiKeyService.ValidateApiKey(c.Request.Context(), apiKey) if err != nil { diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 97562697..bc8de9f5 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -16,13 +16,25 @@ import ( "gorm.io/gorm/clause" ) +const staticApiKeyUserID = "00000000-0000-0000-0000-000000000000" + type ApiKeyService struct { db *gorm.DB emailService *EmailService } -func NewApiKeyService(db *gorm.DB, emailService *EmailService) *ApiKeyService { - return &ApiKeyService{db: db, emailService: emailService} +func NewApiKeyService(ctx context.Context, db *gorm.DB, emailService *EmailService) (*ApiKeyService, error) { + s := &ApiKeyService{db: db, emailService: emailService} + + if common.EnvConfig.StaticApiKey == "" { + err := s.deleteStaticApiKeyUser(ctx) + if err != nil { + return nil, err + } + } + + return s, nil + } func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.ApiKey, utils.PaginationResponse, error) { @@ -144,6 +156,10 @@ func (s *ApiKeyService) ValidateApiKey(ctx context.Context, apiKey string) (mode return model.User{}, &common.NoAPIKeyProvidedError{} } + if common.EnvConfig.StaticApiKey != "" && apiKey == common.EnvConfig.StaticApiKey { + return s.initStaticApiKeyUser(ctx) + } + now := time.Now() hashedKey := utils.CreateSha256Hash(apiKey) @@ -217,3 +233,47 @@ func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey Update("expiration_email_sent", true). Error } + +func (s *ApiKeyService) initStaticApiKeyUser(ctx context.Context) (user model.User, err error) { + err = s.db. + WithContext(ctx). + First(&user, "id = ?", staticApiKeyUserID). + Error + + if err == nil { + return user, nil + } + + if !errors.Is(err, gorm.ErrRecordNotFound) { + return model.User{}, err + } + + usernameSuffix, err := utils.GenerateRandomAlphanumericString(6) + if err != nil { + return model.User{}, err + } + + user = model.User{ + Base: model.Base{ + ID: staticApiKeyUserID, + }, + FirstName: "Static API User", + Username: "static-api-user-" + usernameSuffix, + DisplayName: "Static API User", + IsAdmin: true, + } + + err = s.db. + WithContext(ctx). + Create(&user). + Error + + return user, err +} + +func (s *ApiKeyService) deleteStaticApiKeyUser(ctx context.Context) error { + return s.db. + WithContext(ctx). + Delete(&model.User{}, "id = ?", staticApiKeyUserID). + Error +} diff --git a/backend/internal/service/user_signup_service.go b/backend/internal/service/user_signup_service.go index 604a81b2..1c5974d1 100644 --- a/backend/internal/service/user_signup_service.go +++ b/backend/internal/service/user_signup_service.go @@ -125,7 +125,9 @@ func (s *UserSignUpService) SignUpInitialAdmin(ctx context.Context, signUpData d }() var userCount int64 - if err := tx.WithContext(ctx).Model(&model.User{}).Count(&userCount).Error; err != nil { + if err := tx.WithContext(ctx).Model(&model.User{}). + Where("id != ?", staticApiKeyUserID). + Count(&userCount).Error; err != nil { return model.User{}, "", err } if userCount != 0 { diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index b588dc60..872cc2aa 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -48,10 +48,10 @@ try { await userService.remove(user.id); await refresh(); + toast.success(m.user_deleted_successfully()); } catch (e) { axiosErrorToast(e); } - toast.success(m.user_deleted_successfully()); } } });