mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-19 19:21:28 +02:00
Completed code updates
This commit is contained in:
@@ -216,7 +216,7 @@ func TestAppConfigModel_Update(t *testing.T) {
|
||||
|
||||
err := m.Update(map[string]string{"thisKeyDoesNotExist": "value"})
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, "cannot find config key 'thisKeyDoesNotExist'")
|
||||
require.EqualError(t, err, "cannot find config key 'thisKeyDoesNotExist'")
|
||||
|
||||
notFound, ok := errors.AsType[AppConfigKeyNotFoundError](err)
|
||||
require.True(t, ok)
|
||||
|
||||
@@ -179,19 +179,19 @@ func (s *AppConfigService) loadDbConfigFromEnv() (*AppConfigModel, error) {
|
||||
for i := range rt.NumField() {
|
||||
field := rt.Field(i)
|
||||
|
||||
// Get the key and internal tag values
|
||||
key, attrs, _ := strings.Cut(field.Tag.Get("key"), ",")
|
||||
// Derive the environment variable name from the configuration's JSON key
|
||||
key, _, _ := strings.Cut(field.Tag.Get("json"), ",")
|
||||
envVarName := utils.CamelCaseToScreamingSnakeCase(key)
|
||||
|
||||
// Set the value if it's set
|
||||
value, ok := os.LookupEnv(envVarName)
|
||||
if ok {
|
||||
rv.Field(i).Set(reflect.ValueOf(value))
|
||||
rv.Field(i).SetString(value)
|
||||
continue
|
||||
}
|
||||
|
||||
// If it's sensitive, we also allow reading from file
|
||||
if attrs == "sensitive" {
|
||||
if field.Tag.Get("sensitive") == "true" {
|
||||
fileName := os.Getenv(envVarName + "_FILE")
|
||||
if fileName != "" {
|
||||
// #nosec G703 - Value is provided by admin
|
||||
@@ -200,7 +200,7 @@ func (s *AppConfigService) loadDbConfigFromEnv() (*AppConfigModel, error) {
|
||||
return nil, fmt.Errorf("failed to read secret '%s' from file '%s': %w", envVarName, fileName, err)
|
||||
}
|
||||
|
||||
rv.Field(i).Set(reflect.ValueOf(string(b)))
|
||||
rv.Field(i).SetString(string(b))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ func TestService_NewService(t *testing.T) {
|
||||
|
||||
t.Run("loads config from the environment when the UI config is disabled", func(t *testing.T) {
|
||||
setUIConfigDisabled(t, true)
|
||||
t.Setenv("APP_NAME", "Environment App")
|
||||
|
||||
// No actor host or database is needed when the UI config is disabled
|
||||
svc, err := NewService(t.Context(), nil, nil)
|
||||
@@ -112,7 +113,7 @@ func TestService_NewService(t *testing.T) {
|
||||
|
||||
cfg, err := svc.GetConfig(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *getDefaultConfig(), *cfg)
|
||||
assert.Equal(t, AppConfigValue("Environment App"), cfg.AppName)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
// This file contains utils for unit tests and it's only built when the "unit" tag is set
|
||||
package appconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// NewTestAppConfigService is a function used by tests to create AppConfigService objects with pre-defined configuration values
|
||||
func NewTestAppConfigService(config *AppConfigModel) *AppConfigService {
|
||||
if config == nil {
|
||||
@@ -16,3 +20,16 @@ func NewTestAppConfigService(config *AppConfigModel) *AppConfigService {
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// NewTestContext returns a context that resolves the provided application configuration
|
||||
func NewTestContext(ctx context.Context, config *AppConfigModel) context.Context {
|
||||
if config == nil {
|
||||
config = getDefaultConfig()
|
||||
}
|
||||
|
||||
resolver := appConfigResolver(func(context.Context) (*AppConfigModel, error) {
|
||||
return config, nil
|
||||
})
|
||||
|
||||
return context.WithValue(ctx, appConfigCtxKey{}, resolver)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/frontend"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/controller"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
@@ -140,13 +141,14 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, rateLimitServices
|
||||
}
|
||||
|
||||
// Initialize middleware for specific routes
|
||||
appConfigMiddleware := appconfig.NewAppConfigMiddleware(svc.appConfigService)
|
||||
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyModule, svc.userService, svc.jwtService)
|
||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitServices)
|
||||
apiRateLimitMiddleware := rateLimitMiddleware.Add(middleware.RateLimitAPI)
|
||||
|
||||
apiGroup := r.Group("/api", apiRateLimitMiddleware)
|
||||
baseGroup := r.Group("/", apiRateLimitMiddleware)
|
||||
apiGroup := r.Group("/api", appConfigMiddleware.Add(), apiRateLimitMiddleware)
|
||||
baseGroup := r.Group("/", appConfigMiddleware.Add(), apiRateLimitMiddleware)
|
||||
|
||||
svc.apiKeyModule.RegisterRoutes(apiGroup,
|
||||
authMiddleware.WithAdminNotRequired().Add(),
|
||||
@@ -158,7 +160,7 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, rateLimitServices
|
||||
rateLimitMiddleware.Add(middleware.RateLimitWebauthnReauthenticate),
|
||||
)
|
||||
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, rateLimitMiddleware, svc.userService, svc.oneTimeAccessService, svc.webauthnModule, svc.appConfigService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, rateLimitMiddleware, svc.userService, svc.oneTimeAccessService, svc.webauthnModule)
|
||||
controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService)
|
||||
controller.NewAppImagesController(apiGroup, authMiddleware, svc.appImagesService)
|
||||
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
|
||||
|
||||
@@ -68,24 +68,25 @@ func initServices(
|
||||
svc.appImagesService = service.NewAppImagesService(imageExtensions, fileStorage)
|
||||
svc.appLockService = service.NewAppLockService(db)
|
||||
|
||||
svc.emailService, err = service.NewEmailService(db, svc.appConfigService)
|
||||
svc.emailService, err = service.NewEmailService(db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create email service: %w", err)
|
||||
}
|
||||
|
||||
svc.geoLiteService = service.NewGeoLiteService(httpClient)
|
||||
svc.auditLogService = service.NewAuditLogService(db, svc.appConfigService, svc.emailService, svc.geoLiteService)
|
||||
svc.jwtService, err = service.NewJwtService(ctx, db, instanceID, svc.appConfigService)
|
||||
svc.auditLogService = service.NewAuditLogService(db, svc.emailService, svc.geoLiteService)
|
||||
svc.jwtService, err = service.NewJwtService(ctx, db, instanceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JWT service: %w", err)
|
||||
}
|
||||
|
||||
svc.customClaimService = service.NewCustomClaimService(db)
|
||||
svc.webauthnModule, err = webauthn.New(webauthn.Dependencies{
|
||||
DB: db,
|
||||
AppURL: common.EnvConfig.AppURL,
|
||||
Signer: svc.jwtService,
|
||||
AuditLog: svc.auditLogService,
|
||||
svc.webauthnModule, err = webauthn.New(ctx, webauthn.Dependencies{
|
||||
DB: db,
|
||||
AppURL: common.EnvConfig.AppURL,
|
||||
Signer: svc.jwtService,
|
||||
AuditLog: svc.auditLogService,
|
||||
AppConfig: svc.appConfigService,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create WebAuthn module: %w", err)
|
||||
@@ -114,14 +115,14 @@ func initServices(
|
||||
return nil, fmt.Errorf("failed to create OIDC module: %w", err)
|
||||
}
|
||||
|
||||
svc.oidcService, err = service.NewOidcService(db, svc.jwtService, svc.appConfigService, svc.oidcModule.Preview, svc.scimService, httpClient, fileStorage)
|
||||
svc.oidcService, err = service.NewOidcService(db, svc.jwtService, svc.oidcModule.Preview, svc.scimService, httpClient, fileStorage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
|
||||
}
|
||||
|
||||
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.userGroupService = service.NewUserGroupService(db, svc.scimService)
|
||||
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.customClaimService, svc.appImagesService, svc.scimService, fileStorage)
|
||||
svc.ldapService = service.NewLdapService(db, httpClient, svc.userService, svc.userGroupService, fileStorage)
|
||||
|
||||
svc.apiKeyModule, err = apikey.New(ctx, apikey.Dependencies{
|
||||
DB: db,
|
||||
@@ -135,10 +136,9 @@ func initServices(
|
||||
DB: db,
|
||||
Signer: svc.jwtService,
|
||||
AuditLog: svc.auditLogService,
|
||||
AppConfig: svc.appConfigService,
|
||||
UserCreator: svc.userService,
|
||||
})
|
||||
svc.oneTimeAccessService = service.NewOneTimeAccessService(db, svc.userService, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
|
||||
svc.oneTimeAccessService = service.NewOneTimeAccessService(db, svc.userService, svc.jwtService, svc.auditLogService, svc.emailService)
|
||||
|
||||
svc.versionService = service.NewVersionService(httpClient)
|
||||
|
||||
|
||||
@@ -53,11 +53,12 @@ type AppConfigController struct {
|
||||
// @Success 200 {array} dto.PublicAppConfigVariableDto
|
||||
// @Router /api/application-configuration [get]
|
||||
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||
configuration, err := acc.appConfigService.ListAppConfig(c.Request.Context(), false)
|
||||
dbConfig, err := appconfig.FromCtx(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
configuration := dbConfig.ToAppConfigVariableSlice(false, true)
|
||||
|
||||
var configVariablesDto []dto.PublicAppConfigVariableDto
|
||||
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
||||
@@ -91,11 +92,12 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||
// @Success 200 {array} dto.AppConfigVariableDto
|
||||
// @Router /api/application-configuration/all [get]
|
||||
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
||||
configuration, err := acc.appConfigService.ListAppConfig(c.Request.Context(), true)
|
||||
dbConfig, err := appconfig.FromCtx(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
configuration := dbConfig.ToAppConfigVariableSlice(true, true)
|
||||
|
||||
var configVariablesDto []dto.AppConfigVariableDto
|
||||
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
||||
|
||||
@@ -23,12 +23,11 @@ const defaultOneTimeAccessTokenDuration = 15 * time.Minute
|
||||
// @Summary User management controller
|
||||
// @Description Initializes all user-related API endpoints
|
||||
// @Tags Users
|
||||
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, webAuthnService *webauthn.Module, appConfigService *appconfig.AppConfigService) {
|
||||
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, webAuthnService *webauthn.Module) {
|
||||
uc := UserController{
|
||||
userService: userService,
|
||||
oneTimeAccessService: oneTimeAccessService,
|
||||
webAuthnService: webAuthnService,
|
||||
appConfigService: appConfigService,
|
||||
}
|
||||
|
||||
group.GET("/users", authMiddleware.Add(), uc.listUsersHandler)
|
||||
@@ -66,7 +65,6 @@ type UserController struct {
|
||||
userService *service.UserService
|
||||
oneTimeAccessService *service.OneTimeAccessService
|
||||
webAuthnService *webauthn.Module
|
||||
appConfigService *appconfig.AppConfigService
|
||||
}
|
||||
|
||||
// getUserGroupsHandler godoc
|
||||
|
||||
@@ -137,15 +137,6 @@ func TestMigrateFromAppConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// countLegacyInstanceID returns the number of "instanceId" rows left in the app_config_variables table
|
||||
countLegacyInstanceID := func(t *testing.T, db *gorm.DB) int64 {
|
||||
t.Helper()
|
||||
var count int64
|
||||
err := db.Table("app_config_variables").Where(`"key" = ?`, "instanceId").Count(&count).Error
|
||||
require.NoError(t, err)
|
||||
return count
|
||||
}
|
||||
|
||||
t.Run("moves an existing instance ID from app_config_variables into the kv table", func(t *testing.T) {
|
||||
legacyID := uuid.NewString()
|
||||
db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMove, seedAppConfigInstanceID(legacyID))
|
||||
@@ -155,8 +146,8 @@ func TestMigrateFromAppConfig(t *testing.T) {
|
||||
require.Equal(t, 1, count)
|
||||
require.Equal(t, legacyID, stored)
|
||||
|
||||
// The legacy row must have been removed from app_config_variables
|
||||
require.Zero(t, countLegacyInstanceID(t, db))
|
||||
// The final config migration removes the legacy table after freezing its remaining values
|
||||
require.False(t, db.Migrator().HasTable("app_config_variables"))
|
||||
|
||||
// Load must return the migrated value without generating a new one
|
||||
id, err := Load(t.Context(), db)
|
||||
@@ -184,7 +175,8 @@ func TestMigrateFromAppConfig(t *testing.T) {
|
||||
require.Equal(t, 1, count)
|
||||
require.Equal(t, "kv-instance-id", stored)
|
||||
|
||||
// The legacy row must still be removed regardless of the conflict
|
||||
require.Zero(t, countLegacyInstanceID(t, db))
|
||||
// The final config migration removes the legacy table regardless of the conflict
|
||||
ok := db.Migrator().HasTable("app_config_variables")
|
||||
require.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) err
|
||||
continue
|
||||
}
|
||||
|
||||
err = service.SendEmail(ctx, j.emailService, email.Address{
|
||||
err = service.SendEmailWithConfig(ctx, j.emailService, dbConfig, email.Address{
|
||||
Name: key.User.FullName(),
|
||||
Email: *key.User.Email,
|
||||
}, service.ApiKeyExpiringSoonTemplate, &service.ApiKeyExpiringSoonTemplateData{
|
||||
|
||||
@@ -31,5 +31,5 @@ func (j *LdapJobs) syncLdap(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return j.ldapService.SyncAll(ctx)
|
||||
return j.ldapService.SyncAllWithConfig(ctx, dbConfig)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/apikey"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/instanceid"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
@@ -34,15 +33,13 @@ func TestWithApiKeyAuthDisabled(t *testing.T) {
|
||||
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
appConfigService := appconfig.NewTestAppConfigService(nil)
|
||||
|
||||
instanceID, err := instanceid.Load(t.Context(), db)
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtService, err := service.NewJwtService(t.Context(), db, instanceID, appConfigService)
|
||||
jwtService, err := service.NewJwtService(t.Context(), db, instanceID)
|
||||
require.NoError(t, err)
|
||||
|
||||
userService := service.NewUserService(db, jwtService, nil, nil, appConfigService, nil, nil, nil, nil)
|
||||
userService := service.NewUserService(db, jwtService, nil, nil, nil, nil, nil, nil)
|
||||
apiKeyModule, err := apikey.New(t.Context(), apikey.Dependencies{DB: db})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -14,18 +14,16 @@ import (
|
||||
)
|
||||
|
||||
type AuditLogService struct {
|
||||
db *gorm.DB
|
||||
appConfigService *appconfig.AppConfigService
|
||||
emailService *EmailService
|
||||
geoliteService *GeoLiteService
|
||||
db *gorm.DB
|
||||
emailService *EmailService
|
||||
geoliteService *GeoLiteService
|
||||
}
|
||||
|
||||
func NewAuditLogService(db *gorm.DB, appConfigService *appconfig.AppConfigService, emailService *EmailService, geoliteService *GeoLiteService) *AuditLogService {
|
||||
func NewAuditLogService(db *gorm.DB, emailService *EmailService, geoliteService *GeoLiteService) *AuditLogService {
|
||||
return &AuditLogService{
|
||||
db: db,
|
||||
appConfigService: appConfigService,
|
||||
emailService: emailService,
|
||||
geoliteService: geoliteService,
|
||||
db: db,
|
||||
emailService: emailService,
|
||||
geoliteService: geoliteService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/api"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/oidc"
|
||||
@@ -625,8 +626,8 @@ func (s *TestService) ResetApplicationImages(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *TestService) ResetAppConfig(ctx context.Context) error {
|
||||
// Reset all app config variables to their default values in the database
|
||||
err := s.db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&model.AppConfigVariable{}).Update("value", "").Error
|
||||
// Reset all application configuration values through the singleton actor
|
||||
_, err := s.appConfigService.UpdateAppConfig(ctx, dto.AppConfigUpdateDto{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -647,12 +648,6 @@ func (s *TestService) ResetAppConfig(ctx context.Context) error {
|
||||
// The instance ID is loaded once at startup, so we also set it directly on the JWT service so it takes effect immediately
|
||||
s.jwtService.instanceID = testInstanceID
|
||||
|
||||
// Reload the app config from the database after resetting the values
|
||||
err = s.appConfigService.LoadDbConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reload the JWK
|
||||
if err := s.jwtService.LoadOrGenerateKey(ctx); err != nil {
|
||||
return err
|
||||
@@ -671,48 +666,32 @@ func (s *TestService) SyncLdap(ctx context.Context) error {
|
||||
return s.ldapService.SyncAll(ctx)
|
||||
}
|
||||
|
||||
// SetLdapTestConfig writes the test LDAP config variables directly to the database.
|
||||
// SetLdapTestConfig updates the LDAP configuration used by the end-to-end test server
|
||||
func (s *TestService) SetLdapTestConfig(ctx context.Context) error {
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
ldapConfigs := map[string]string{
|
||||
"ldapUrl": "ldap://lldap:3890",
|
||||
"ldapBindDn": "uid=admin,ou=people,dc=pocket-id,dc=org",
|
||||
"ldapBindPassword": "admin_password",
|
||||
"ldapBase": "dc=pocket-id,dc=org",
|
||||
"ldapUserSearchFilter": "(objectClass=person)",
|
||||
"ldapUserGroupSearchFilter": "(objectClass=groupOfNames)",
|
||||
"ldapSkipCertVerify": "true",
|
||||
"ldapAttributeUserUniqueIdentifier": "uuid",
|
||||
"ldapAttributeUserUsername": "uid",
|
||||
"ldapAttributeUserEmail": "mail",
|
||||
"ldapAttributeUserFirstName": "givenName",
|
||||
"ldapAttributeUserLastName": "sn",
|
||||
"ldapAttributeGroupUniqueIdentifier": "uuid",
|
||||
"ldapAttributeGroupName": "uid",
|
||||
"ldapAttributeGroupMember": "member",
|
||||
"ldapAdminGroupName": "admin_group",
|
||||
"ldapSoftDeleteUsers": "true",
|
||||
"ldapEnabled": "true",
|
||||
}
|
||||
|
||||
for key, value := range ldapConfigs {
|
||||
configVar := appconfig.AppConfigVariable{Key: key, Value: value}
|
||||
if err := tx.Create(&configVar).Error; err != nil {
|
||||
return fmt.Errorf("failed to create config variable '%s': %w", key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := s.appConfigService.UpdateAppConfigValues(ctx,
|
||||
"ldapUrl", "ldap://lldap:3890",
|
||||
"ldapBindDn", "uid=admin,ou=people,dc=pocket-id,dc=org",
|
||||
"ldapBindPassword", "admin_password",
|
||||
"ldapBase", "dc=pocket-id,dc=org",
|
||||
"ldapUserSearchFilter", "(objectClass=person)",
|
||||
"ldapUserGroupSearchFilter", "(objectClass=groupOfNames)",
|
||||
"ldapSkipCertVerify", "true",
|
||||
"ldapAttributeUserUniqueIdentifier", "uuid",
|
||||
"ldapAttributeUserUsername", "uid",
|
||||
"ldapAttributeUserEmail", "mail",
|
||||
"ldapAttributeUserFirstName", "givenName",
|
||||
"ldapAttributeUserLastName", "sn",
|
||||
"ldapAttributeGroupUniqueIdentifier", "uuid",
|
||||
"ldapAttributeGroupName", "uid",
|
||||
"ldapAttributeGroupMember", "member",
|
||||
"ldapAdminGroupName", "admin_group",
|
||||
"ldapSoftDeleteUsers", "true",
|
||||
"ldapEnabled", "true",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set LDAP test config: %w", err)
|
||||
}
|
||||
|
||||
err = s.appConfigService.LoadDbConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load app config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,12 @@ import (
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
appConfigService *appconfig.AppConfigService
|
||||
db *gorm.DB
|
||||
htmlTemplates map[string]*htemplate.Template
|
||||
textTemplates map[string]*ttemplate.Template
|
||||
db *gorm.DB
|
||||
htmlTemplates map[string]*htemplate.Template
|
||||
textTemplates map[string]*ttemplate.Template
|
||||
}
|
||||
|
||||
func NewEmailService(db *gorm.DB, appConfigService *appconfig.AppConfigService) (*EmailService, error) {
|
||||
func NewEmailService(db *gorm.DB) (*EmailService, error) {
|
||||
htmlTemplates, err := email.PrepareHTMLTemplates(emailTemplatesPaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare html templates: %w", err)
|
||||
@@ -38,10 +37,9 @@ func NewEmailService(db *gorm.DB, appConfigService *appconfig.AppConfigService)
|
||||
}
|
||||
|
||||
return &EmailService{
|
||||
appConfigService: appConfigService,
|
||||
db: db,
|
||||
htmlTemplates: htmlTemplates,
|
||||
textTemplates: textTemplates,
|
||||
db: db,
|
||||
htmlTemplates: htmlTemplates,
|
||||
textTemplates: textTemplates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -67,10 +65,19 @@ func (srv *EmailService) SendTestEmail(ctx context.Context, recipientUserId stri
|
||||
}
|
||||
|
||||
func SendEmail[V any](ctx context.Context, srv *EmailService, toEmail email.Address, template email.Template[V], tData *V) error {
|
||||
dbConfig := srv.appConfigService.GetDbConfig()
|
||||
dbConfig, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
return SendEmailWithConfig(ctx, srv, dbConfig, toEmail, template, tData)
|
||||
}
|
||||
|
||||
// SendEmailWithConfig sends an email with an explicitly loaded configuration for call chains that do not originate from an HTTP request
|
||||
func SendEmailWithConfig[V any](ctx context.Context, srv *EmailService, dbConfig *appconfig.AppConfigModel, toEmail email.Address, template email.Template[V], tData *V) error {
|
||||
|
||||
data := &email.TemplateData[V]{
|
||||
AppName: dbConfig.AppName.Value,
|
||||
AppName: dbConfig.AppName.String(),
|
||||
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
|
||||
Data: tData,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
|
||||
@@ -44,19 +43,18 @@ const (
|
||||
)
|
||||
|
||||
type JwtService struct {
|
||||
db *gorm.DB
|
||||
envConfig *common.EnvConfigSchema
|
||||
privateKey jwk.Key
|
||||
keyId string
|
||||
appConfigService *appconfig.AppConfigService
|
||||
instanceID string
|
||||
jwksEncoded []byte
|
||||
db *gorm.DB
|
||||
envConfig *common.EnvConfigSchema
|
||||
privateKey jwk.Key
|
||||
keyId string
|
||||
instanceID string
|
||||
jwksEncoded []byte
|
||||
}
|
||||
|
||||
func NewJwtService(ctx context.Context, db *gorm.DB, instanceID string, appConfigService *appconfig.AppConfigService) (*JwtService, error) {
|
||||
func NewJwtService(ctx context.Context, db *gorm.DB, instanceID string) (*JwtService, error) {
|
||||
service := &JwtService{}
|
||||
|
||||
err := service.init(ctx, db, instanceID, appConfigService, &common.EnvConfig)
|
||||
err := service.init(ctx, db, instanceID, &common.EnvConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,8 +62,7 @@ func NewJwtService(ctx context.Context, db *gorm.DB, instanceID string, appConfi
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *JwtService) init(ctx context.Context, db *gorm.DB, instanceID string, appConfigService *appconfig.AppConfigService, envConfig *common.EnvConfigSchema) (err error) {
|
||||
s.appConfigService = appConfigService
|
||||
func (s *JwtService) init(ctx context.Context, db *gorm.DB, instanceID string, envConfig *common.EnvConfigSchema) (err error) {
|
||||
s.envConfig = envConfig
|
||||
s.db = db
|
||||
s.instanceID = instanceID
|
||||
|
||||
@@ -36,11 +36,11 @@ func newTestEnvConfig() *common.EnvConfigSchema {
|
||||
}
|
||||
}
|
||||
|
||||
func initJwtService(t *testing.T, db *gorm.DB, instanceID string, appConfig *appconfig.AppConfigService, envConfig *common.EnvConfigSchema) *JwtService {
|
||||
func initJwtService(t *testing.T, db *gorm.DB, instanceID string, _ *appconfig.AppConfigService, envConfig *common.EnvConfigSchema) *JwtService {
|
||||
t.Helper()
|
||||
|
||||
service := &JwtService{}
|
||||
err := service.init(t.Context(), db, instanceID, appConfig, envConfig)
|
||||
err := service.init(t.Context(), db, instanceID, envConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
return service
|
||||
|
||||
@@ -30,13 +30,12 @@ import (
|
||||
)
|
||||
|
||||
type LdapService struct {
|
||||
db *gorm.DB
|
||||
httpClient *http.Client
|
||||
appConfigService *appconfig.AppConfigService
|
||||
userService *UserService
|
||||
groupService *UserGroupService
|
||||
fileStorage storage.FileStorage
|
||||
clientFactory func(dbConfig *appconfig.AppConfigModel) (ldapClient, error)
|
||||
db *gorm.DB
|
||||
httpClient *http.Client
|
||||
userService *UserService
|
||||
groupService *UserGroupService
|
||||
fileStorage storage.FileStorage
|
||||
clientFactory func(dbConfig *appconfig.AppConfigModel) (ldapClient, error)
|
||||
}
|
||||
|
||||
type savePicture struct {
|
||||
@@ -70,14 +69,13 @@ type ldapClient interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
func NewLdapService(db *gorm.DB, httpClient *http.Client, appConfigService *appconfig.AppConfigService, userService *UserService, groupService *UserGroupService, fileStorage storage.FileStorage) *LdapService {
|
||||
func NewLdapService(db *gorm.DB, httpClient *http.Client, userService *UserService, groupService *UserGroupService, fileStorage storage.FileStorage) *LdapService {
|
||||
service := &LdapService{
|
||||
db: db,
|
||||
httpClient: httpClient,
|
||||
appConfigService: appConfigService,
|
||||
userService: userService,
|
||||
groupService: groupService,
|
||||
fileStorage: fileStorage,
|
||||
db: db,
|
||||
httpClient: httpClient,
|
||||
userService: userService,
|
||||
groupService: groupService,
|
||||
fileStorage: fileStorage,
|
||||
}
|
||||
|
||||
service.clientFactory = service.createClient
|
||||
@@ -110,7 +108,15 @@ func (s *LdapService) SyncAll(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
return s.syncAll(ctx, dbConfig)
|
||||
}
|
||||
|
||||
// SyncAllWithConfig synchronizes LDAP with an explicitly loaded configuration for call chains that do not originate from an HTTP request
|
||||
func (s *LdapService) SyncAllWithConfig(ctx context.Context, dbConfig *appconfig.AppConfigModel) error {
|
||||
return s.syncAll(ctx, dbConfig)
|
||||
}
|
||||
|
||||
func (s *LdapService) syncAll(ctx context.Context, dbConfig *appconfig.AppConfigModel) error {
|
||||
// Setup LDAP connection
|
||||
client, err := s.clientFactory(dbConfig)
|
||||
if err != nil {
|
||||
@@ -173,7 +179,7 @@ func (s *LdapService) SyncAll(ctx context.Context) error {
|
||||
|
||||
func (s *LdapService) fetchDesiredState(ctx context.Context, client ldapClient, dbConfig *appconfig.AppConfigModel) (ldapDesiredState, error) {
|
||||
// Fetch users first so we can use their DNs when resolving group members
|
||||
users, userIDs, usernamesByDN, err := s.fetchUsersFromLDAP(ctx, client)
|
||||
users, userIDs, usernamesByDN, err := s.fetchUsersFromLDAP(ctx, client, dbConfig)
|
||||
if err != nil {
|
||||
return ldapDesiredState{}, err
|
||||
}
|
||||
@@ -290,12 +296,7 @@ func (s *LdapService) fetchGroupsFromLDAP(ctx context.Context, client ldapClient
|
||||
return desiredGroups, ldapGroupIDs, nil
|
||||
}
|
||||
|
||||
func (s *LdapService) fetchUsersFromLDAP(ctx context.Context, client ldapClient) (desiredUsers []ldapDesiredUser, ldapUserIDs map[string]struct{}, usernamesByDN map[string]string, err error) {
|
||||
dbConfig, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
func (s *LdapService) fetchUsersFromLDAP(ctx context.Context, client ldapClient, dbConfig *appconfig.AppConfigModel) (desiredUsers []ldapDesiredUser, ldapUserIDs map[string]struct{}, usernamesByDN map[string]string, err error) {
|
||||
// Query LDAP for all users we want to manage
|
||||
searchAttrs := []string{
|
||||
"sn",
|
||||
@@ -532,7 +533,7 @@ func (s *LdapService) reconcileUsers(ctx context.Context, tx *gorm.DB, desiredUs
|
||||
|
||||
userID := databaseUser.ID
|
||||
if databaseUser.ID == "" {
|
||||
createdUser, err := s.userService.CreateUserInternal(ctx, desiredUser.input, true, tx)
|
||||
createdUser, err := s.userService.createUserInternal(ctx, desiredUser.input, true, tx, dbConfig)
|
||||
if errors.Is(err, &common.AlreadyInUseError{}) {
|
||||
slog.Warn("Skipping creating LDAP user", slog.String("username", desiredUser.input.Username), slog.Any("error", err))
|
||||
continue
|
||||
@@ -543,7 +544,7 @@ func (s *LdapService) reconcileUsers(ctx context.Context, tx *gorm.DB, desiredUs
|
||||
userID = createdUser.ID
|
||||
ldapUsersByID[desiredUser.ldapID] = createdUser
|
||||
} else {
|
||||
_, err = s.userService.updateUserInternal(ctx, databaseUser.ID, desiredUser.input, false, true, tx)
|
||||
_, err = s.userService.updateUserInternal(ctx, databaseUser.ID, desiredUser.input, false, true, tx, dbConfig)
|
||||
if errors.Is(err, &common.AlreadyInUseError{}) {
|
||||
slog.Warn("Skipping updating LDAP user", slog.String("username", desiredUser.input.Username), slog.Any("error", err))
|
||||
continue
|
||||
|
||||
@@ -110,7 +110,8 @@ func TestLdapServiceSyncAllReconcilesUsersAndGroups(t *testing.T) {
|
||||
LdapID: &oldGroupLdapID,
|
||||
}).Error)
|
||||
|
||||
require.NoError(t, service.SyncAll(t.Context()))
|
||||
err := service.SyncAllWithConfig(t.Context(), defaultTestLDAPAppConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
var alice model.User
|
||||
require.NoError(t, db.First(&alice, "ldap_id = ?", aliceLdapID).Error)
|
||||
@@ -176,7 +177,8 @@ func TestLdapServiceSyncAllMapsPosixGroupMemberUid(t *testing.T) {
|
||||
),
|
||||
))
|
||||
|
||||
require.NoError(t, service.SyncAll(t.Context()))
|
||||
err := service.SyncAllWithConfig(t.Context(), appCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var group model.UserGroup
|
||||
require.NoError(t, db.Preload("Users").First(&group, "ldap_id = ?", "g-users").Error)
|
||||
@@ -218,7 +220,8 @@ func TestLdapServiceSyncAllHandlesDuplicateLDAPIDsInSingleRun(t *testing.T) {
|
||||
),
|
||||
))
|
||||
|
||||
require.NoError(t, service.SyncAll(t.Context()))
|
||||
err := service.SyncAllWithConfig(t.Context(), defaultTestLDAPAppConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
var users []model.User
|
||||
require.NoError(t, db.Find(&users, "ldap_id = ?", "u-dup").Error)
|
||||
@@ -289,7 +292,8 @@ func TestLdapServiceSyncAllSetsAdminFromGroupMembership(t *testing.T) {
|
||||
ldapSearchResult(tt.groupEntry),
|
||||
))
|
||||
|
||||
require.NoError(t, service.SyncAll(t.Context()))
|
||||
err := service.SyncAllWithConfig(t.Context(), tt.appConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var user model.User
|
||||
require.NoError(t, db.First(&user, "ldap_id = ?", "u-testadmin").Error)
|
||||
@@ -317,22 +321,19 @@ func newTestLdapServiceWithAppConfig(t *testing.T, appConfigModel *appconfig.App
|
||||
fileStorage, err := storage.NewDatabaseStorage(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
appConfig := appconfig.NewTestAppConfigService(appConfigModel)
|
||||
|
||||
groupService := NewUserGroupService(db, appConfig, nil)
|
||||
groupService := NewUserGroupService(db, nil)
|
||||
userService := NewUserService(
|
||||
db,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
appConfig,
|
||||
NewCustomClaimService(db),
|
||||
NewAppImagesService(map[string]string{}, fileStorage),
|
||||
nil,
|
||||
fileStorage,
|
||||
)
|
||||
|
||||
service := NewLdapService(db, &http.Client{}, appConfig, userService, groupService, fileStorage)
|
||||
service := NewLdapService(db, &http.Client{}, userService, groupService, fileStorage)
|
||||
service.clientFactory = func(dbConfig *appconfig.AppConfigModel) (ldapClient, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
@@ -38,11 +37,10 @@ const (
|
||||
)
|
||||
|
||||
type OidcService struct {
|
||||
db *gorm.DB
|
||||
jwtService *JwtService
|
||||
appConfigService *appconfig.AppConfigService
|
||||
previewBuilder oidcClientPreviewBuilder
|
||||
scimService *ScimService
|
||||
db *gorm.DB
|
||||
jwtService *JwtService
|
||||
previewBuilder oidcClientPreviewBuilder
|
||||
scimService *ScimService
|
||||
|
||||
httpClient *http.Client
|
||||
fileStorage storage.FileStorage
|
||||
@@ -55,20 +53,18 @@ type oidcClientPreviewBuilder interface {
|
||||
func NewOidcService(
|
||||
db *gorm.DB,
|
||||
jwtService *JwtService,
|
||||
appConfigService *appconfig.AppConfigService,
|
||||
previewBuilder oidcClientPreviewBuilder,
|
||||
scimService *ScimService,
|
||||
httpClient *http.Client,
|
||||
fileStorage storage.FileStorage,
|
||||
) (s *OidcService, err error) {
|
||||
s = &OidcService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
appConfigService: appConfigService,
|
||||
previewBuilder: previewBuilder,
|
||||
scimService: scimService,
|
||||
httpClient: httpClient,
|
||||
fileStorage: fileStorage,
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
previewBuilder: previewBuilder,
|
||||
scimService: scimService,
|
||||
httpClient: httpClient,
|
||||
fileStorage: fileStorage,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
@@ -454,7 +454,7 @@ func TestOidcService_downloadAndSaveLogoFromURL(t *testing.T) {
|
||||
func TestOidcService_CreateClient_withDescription(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
description := "A test client description"
|
||||
@@ -479,7 +479,7 @@ func TestOidcService_CreateClient_withDescription(t *testing.T) {
|
||||
func TestOidcService_CreateClient_withoutDescription(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
input := dto.OidcClientCreateDto{
|
||||
@@ -501,7 +501,7 @@ func TestOidcService_CreateClient_withoutDescription(t *testing.T) {
|
||||
func TestOidcService_UpdateClient_description(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
|
||||
s, err := NewOidcService(db, nil, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a client without a description
|
||||
|
||||
@@ -20,22 +20,20 @@ import (
|
||||
)
|
||||
|
||||
type OneTimeAccessService struct {
|
||||
db *gorm.DB
|
||||
userService *UserService
|
||||
appConfigService *appconfig.AppConfigService
|
||||
jwtService *JwtService
|
||||
auditLogService *AuditLogService
|
||||
emailService *EmailService
|
||||
db *gorm.DB
|
||||
userService *UserService
|
||||
jwtService *JwtService
|
||||
auditLogService *AuditLogService
|
||||
emailService *EmailService
|
||||
}
|
||||
|
||||
func NewOneTimeAccessService(db *gorm.DB, userService *UserService, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *appconfig.AppConfigService) *OneTimeAccessService {
|
||||
func NewOneTimeAccessService(db *gorm.DB, userService *UserService, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService) *OneTimeAccessService {
|
||||
return &OneTimeAccessService{
|
||||
db: db,
|
||||
userService: userService,
|
||||
appConfigService: appConfigService,
|
||||
jwtService: jwtService,
|
||||
auditLogService: auditLogService,
|
||||
emailService: emailService,
|
||||
db: db,
|
||||
userService: userService,
|
||||
jwtService: jwtService,
|
||||
auditLogService: auditLogService,
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ func TestExchangeOneTimeAccessTokenRejectsDisabledUser(t *testing.T) {
|
||||
appConfig := appconfig.NewTestAppConfigService(nil)
|
||||
instanceID := newInstanceID(t, db)
|
||||
jwtService := initJwtService(t, db, instanceID, appConfig, newTestEnvConfig())
|
||||
auditLogService := NewAuditLogService(db, appConfig, nil, &GeoLiteService{})
|
||||
oneTimeAccessService := NewOneTimeAccessService(db, nil, jwtService, auditLogService, nil, appConfig)
|
||||
auditLogService := NewAuditLogService(db, nil, &GeoLiteService{})
|
||||
oneTimeAccessService := NewOneTimeAccessService(db, nil, jwtService, auditLogService, nil)
|
||||
|
||||
user := model.User{
|
||||
Base: model.Base{ID: "disabled-user"},
|
||||
@@ -36,7 +36,8 @@ func TestExchangeOneTimeAccessTokenRejectsDisabledUser(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, db.Create(&loginCode).Error)
|
||||
|
||||
exchangedUser, accessToken, err := oneTimeAccessService.ExchangeOneTimeAccessToken(t.Context(), loginCode.Token, "", "", "")
|
||||
ctx := appconfig.NewTestContext(t.Context(), nil)
|
||||
exchangedUser, accessToken, err := oneTimeAccessService.ExchangeOneTimeAccessToken(ctx, loginCode.Token, "", "", "")
|
||||
|
||||
var userDisabledErr *common.UserDisabledError
|
||||
require.ErrorAs(t, err, &userDisabledErr)
|
||||
|
||||
@@ -17,13 +17,12 @@ import (
|
||||
)
|
||||
|
||||
type UserGroupService struct {
|
||||
db *gorm.DB
|
||||
scimService *ScimService
|
||||
appConfigService *appconfig.AppConfigService
|
||||
db *gorm.DB
|
||||
scimService *ScimService
|
||||
}
|
||||
|
||||
func NewUserGroupService(db *gorm.DB, appConfigService *appconfig.AppConfigService, scimService *ScimService) *UserGroupService {
|
||||
return &UserGroupService{db: db, appConfigService: appConfigService, scimService: scimService}
|
||||
func NewUserGroupService(db *gorm.DB, scimService *ScimService) *UserGroupService {
|
||||
return &UserGroupService{db: db, scimService: scimService}
|
||||
}
|
||||
|
||||
func (s *UserGroupService) List(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
|
||||
@@ -167,14 +166,15 @@ func (s *UserGroupService) updateInternal(ctx context.Context, id string, input
|
||||
return model.UserGroup{}, err
|
||||
}
|
||||
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return model.UserGroup{}, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
// Disallow updating the group if it is an LDAP group and LDAP is enabled
|
||||
if !isLdapSync && group.LdapID != nil && cfg.LdapEnabled.IsTrue() {
|
||||
return model.UserGroup{}, &common.LdapUserGroupUpdateError{}
|
||||
if !isLdapSync && group.LdapID != nil {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return model.UserGroup{}, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
if cfg.LdapEnabled.IsTrue() {
|
||||
return model.UserGroup{}, &common.LdapUserGroupUpdateError{}
|
||||
}
|
||||
}
|
||||
|
||||
group.Name = input.Name
|
||||
|
||||
@@ -32,20 +32,18 @@ type UserService struct {
|
||||
jwtService *JwtService
|
||||
auditLogService *AuditLogService
|
||||
emailService *EmailService
|
||||
appConfigService *appconfig.AppConfigService
|
||||
customClaimService *CustomClaimService
|
||||
appImagesService *AppImagesService
|
||||
scimService *ScimService
|
||||
fileStorage storage.FileStorage
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *appconfig.AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService, scimService *ScimService, fileStorage storage.FileStorage) *UserService {
|
||||
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, customClaimService *CustomClaimService, appImagesService *AppImagesService, scimService *ScimService, fileStorage storage.FileStorage) *UserService {
|
||||
return &UserService{
|
||||
db: db,
|
||||
jwtService: jwtService,
|
||||
auditLogService: auditLogService,
|
||||
emailService: emailService,
|
||||
appConfigService: appConfigService,
|
||||
customClaimService: customClaimService,
|
||||
appImagesService: appImagesService,
|
||||
scimService: scimService,
|
||||
@@ -205,13 +203,8 @@ func (s *UserService) DeleteUser(ctx context.Context, userID string, allowLdapDe
|
||||
}
|
||||
|
||||
func (s *UserService) deleteUserInternal(ctx context.Context, tx *gorm.DB, userID string, allowLdapDelete bool) error {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
var user model.User
|
||||
err = tx.
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Where("id = ?", userID).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
@@ -222,8 +215,14 @@ func (s *UserService) deleteUserInternal(ctx context.Context, tx *gorm.DB, userI
|
||||
}
|
||||
|
||||
// Disallow deleting the user if it is an LDAP user, LDAP is enabled, and the user is not disabled
|
||||
if !allowLdapDelete && !user.Disabled && user.LdapID != nil && cfg.LdapEnabled.IsTrue() {
|
||||
return &common.LdapUserUpdateError{}
|
||||
if !allowLdapDelete && !user.Disabled && user.LdapID != nil {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
if cfg.LdapEnabled.IsTrue() {
|
||||
return &common.LdapUserUpdateError{}
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.WithContext(ctx).Delete(&user).Error
|
||||
@@ -262,7 +261,10 @@ func (s *UserService) CreateUserInternal(ctx context.Context, input dto.UserCrea
|
||||
if err != nil {
|
||||
return model.User{}, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
return s.createUserInternal(ctx, input, isLdapSync, tx, cfg)
|
||||
}
|
||||
|
||||
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB, cfg *appconfig.AppConfigModel) (model.User, error) {
|
||||
if cfg.RequireUserEmail.IsTrue() && input.Email == nil {
|
||||
return model.User{}, &common.UserEmailNotSetError{}
|
||||
}
|
||||
@@ -295,7 +297,7 @@ func (s *UserService) CreateUserInternal(ctx context.Context, input dto.UserCrea
|
||||
user.LdapID = &input.LdapID
|
||||
}
|
||||
|
||||
err = tx.WithContext(ctx).Create(&user).Error
|
||||
err := tx.WithContext(ctx).Create(&user).Error
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
// Do not follow this path if we're using LDAP, as we don't want to roll-back the transaction here
|
||||
if !isLdapSync {
|
||||
@@ -323,13 +325,13 @@ func (s *UserService) CreateUserInternal(ctx context.Context, input dto.UserCrea
|
||||
// Apply default groups and claims for new non-LDAP users
|
||||
if !isLdapSync {
|
||||
if len(input.UserGroupIds) == 0 {
|
||||
err = s.applyDefaultGroups(ctx, &user, tx)
|
||||
err = s.applyDefaultGroups(ctx, &user, tx, cfg)
|
||||
if err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.applyDefaultCustomClaims(ctx, &user, tx)
|
||||
err = s.applyDefaultCustomClaims(ctx, &user, tx, cfg)
|
||||
if err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
@@ -342,16 +344,11 @@ func (s *UserService) CreateUserInternal(ctx context.Context, input dto.UserCrea
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) applyDefaultGroups(ctx context.Context, user *model.User, tx *gorm.DB) error {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
func (s *UserService) applyDefaultGroups(ctx context.Context, user *model.User, tx *gorm.DB, cfg *appconfig.AppConfigModel) error {
|
||||
var groupIDs []string
|
||||
v := cfg.SignupDefaultUserGroupIDs
|
||||
if v != "" && v != "[]" {
|
||||
err = json.Unmarshal([]byte(v), &groupIDs)
|
||||
err := json.Unmarshal([]byte(v), &groupIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SignupDefaultUserGroupIDs JSON: %w", err)
|
||||
}
|
||||
@@ -409,16 +406,11 @@ func (s *UserService) touchUserGroups(ctx context.Context, tx *gorm.DB, ids []st
|
||||
Error
|
||||
}
|
||||
|
||||
func (s *UserService) applyDefaultCustomClaims(ctx context.Context, user *model.User, tx *gorm.DB) error {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
func (s *UserService) applyDefaultCustomClaims(ctx context.Context, user *model.User, tx *gorm.DB, cfg *appconfig.AppConfigModel) error {
|
||||
var claims []dto.CustomClaimCreateDto
|
||||
v := cfg.SignupDefaultCustomClaims
|
||||
if v != "" && v != "[]" {
|
||||
err = json.Unmarshal([]byte(v), &claims)
|
||||
err := json.Unmarshal([]byte(v), &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SignupDefaultCustomClaims JSON: %w", err)
|
||||
}
|
||||
@@ -434,12 +426,17 @@ func (s *UserService) applyDefaultCustomClaims(ctx context.Context, user *model.
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool) (model.User, error) {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return model.User{}, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
user, err := s.updateUserInternal(ctx, userID, updatedUser, updateOwnUser, isLdapSync, tx)
|
||||
user, err := s.updateUserInternal(ctx, userID, updatedUser, updateOwnUser, isLdapSync, tx, cfg)
|
||||
if err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
@@ -452,18 +449,13 @@ func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) updateUserInternal(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
||||
cfg, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return model.User{}, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
func (s *UserService) updateUserInternal(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool, tx *gorm.DB, cfg *appconfig.AppConfigModel) (model.User, error) {
|
||||
if cfg.RequireUserEmail.IsTrue() && updatedUser.Email == nil {
|
||||
return model.User{}, &common.UserEmailNotSetError{}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
err = tx.
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Where("id = ?", userID).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
||||
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
||||
)
|
||||
|
||||
func newTestUserService(t *testing.T, appConfig *AppConfigService) (*UserService, *UserGroupService) {
|
||||
func newTestUserService(t *testing.T) (*UserService, *UserGroupService) {
|
||||
t.Helper()
|
||||
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
@@ -25,22 +25,20 @@ func newTestUserService(t *testing.T, appConfig *AppConfigService) (*UserService
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
appConfig,
|
||||
NewCustomClaimService(db),
|
||||
NewAppImagesService(map[string]string{}, fileStorage),
|
||||
nil,
|
||||
fileStorage,
|
||||
)
|
||||
groupService := NewUserGroupService(db, appConfig, nil)
|
||||
groupService := NewUserGroupService(db, nil)
|
||||
|
||||
return userService, groupService
|
||||
}
|
||||
|
||||
func TestCreateUserBumpsGroupUpdatedAt(t *testing.T) {
|
||||
appConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
RequireUserEmail: model.AppConfigVariable{Value: "false"},
|
||||
})
|
||||
userService, groupService := newTestUserService(t, appConfig)
|
||||
config := &appconfig.AppConfigModel{RequireUserEmail: "false"}
|
||||
ctx := appconfig.NewTestContext(t.Context(), config)
|
||||
userService, groupService := newTestUserService(t)
|
||||
|
||||
group, err := groupService.Create(t.Context(), dto.UserGroupCreateDto{
|
||||
Name: "members",
|
||||
@@ -52,7 +50,7 @@ func TestCreateUserBumpsGroupUpdatedAt(t *testing.T) {
|
||||
// Create a user that is a member of the group
|
||||
// This mirrors signing up via an invite link that adds the user to a group
|
||||
email := "member@example.com"
|
||||
_, err = userService.CreateUser(t.Context(), dto.UserCreateDto{
|
||||
_, err = userService.CreateUser(ctx, dto.UserCreateDto{
|
||||
Username: "member",
|
||||
Email: &email,
|
||||
FirstName: "Group",
|
||||
@@ -70,10 +68,9 @@ func TestCreateUserBumpsGroupUpdatedAt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateUserBumpsDefaultGroupUpdatedAt(t *testing.T) {
|
||||
appConfig := NewTestAppConfigService(&model.AppConfig{
|
||||
RequireUserEmail: model.AppConfigVariable{Value: "false"},
|
||||
})
|
||||
userService, groupService := newTestUserService(t, appConfig)
|
||||
config := &appconfig.AppConfigModel{RequireUserEmail: "false"}
|
||||
ctx := appconfig.NewTestContext(t.Context(), config)
|
||||
userService, groupService := newTestUserService(t)
|
||||
|
||||
group, err := groupService.Create(t.Context(), dto.UserGroupCreateDto{
|
||||
Name: "default",
|
||||
@@ -85,11 +82,11 @@ func TestCreateUserBumpsDefaultGroupUpdatedAt(t *testing.T) {
|
||||
// Configure the group as a default signup group
|
||||
defaultGroups, err := json.Marshal([]string{group.ID})
|
||||
require.NoError(t, err)
|
||||
appConfig.dbConfig.Load().SignupDefaultUserGroupIDs.Value = string(defaultGroups)
|
||||
config.SignupDefaultUserGroupIDs = appconfig.AppConfigValue(defaultGroups)
|
||||
|
||||
// Create a user without explicit group IDs, so the default groups apply
|
||||
email := "default@example.com"
|
||||
_, err = userService.CreateUser(t.Context(), dto.UserCreateDto{
|
||||
_, err = userService.CreateUser(ctx, dto.UserCreateDto{
|
||||
Username: "defaultmember",
|
||||
Email: &email,
|
||||
FirstName: "Default",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package usersignup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
@@ -15,12 +17,11 @@ import (
|
||||
const defaultSignupTokenDuration = time.Hour
|
||||
|
||||
type handler struct {
|
||||
service *Service
|
||||
appConfig AppConfigProvider
|
||||
service *Service
|
||||
}
|
||||
|
||||
func newHandler(service *Service, appConfig AppConfigProvider) *handler {
|
||||
return &handler{service: service, appConfig: appConfig}
|
||||
func newHandler(service *Service) *handler {
|
||||
return &handler{service: service}
|
||||
}
|
||||
|
||||
func (h *handler) checkInitialAdminSetupAvailable(c *gin.Context) {
|
||||
@@ -48,6 +49,12 @@ func (h *handler) checkInitialAdminSetupAvailable(c *gin.Context) {
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /api/signup/setup [post]
|
||||
func (h *handler) signUpInitialAdmin(c *gin.Context) {
|
||||
config, err := appconfig.FromCtx(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var input signUpDto
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
@@ -66,7 +73,7 @@ func (h *handler) signUpInitialAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
maxAge := int(h.appConfig.GetDbConfig().SessionDuration.AsDurationMinutes().Seconds())
|
||||
maxAge := int(config.SessionDuration.AsDurationMinutes().Seconds())
|
||||
cookie.AddAccessTokenCookie(c, maxAge, token)
|
||||
|
||||
c.JSON(http.StatusOK, userDto)
|
||||
@@ -169,6 +176,12 @@ func (h *handler) deleteSignupToken(c *gin.Context) {
|
||||
// @Success 201 {object} dto.UserDto
|
||||
// @Router /api/signup [post]
|
||||
func (h *handler) signup(c *gin.Context) {
|
||||
config, err := appconfig.FromCtx(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var input signUpDto
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
@@ -184,7 +197,7 @@ func (h *handler) signup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
maxAge := int(h.appConfig.GetDbConfig().SessionDuration.AsDurationMinutes().Seconds())
|
||||
maxAge := int(config.SessionDuration.AsDurationMinutes().Seconds())
|
||||
cookie.AddAccessTokenCookie(c, maxAge, accessToken)
|
||||
|
||||
var userDto dto.UserDto
|
||||
|
||||
@@ -2,6 +2,7 @@ package usersignup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -11,17 +12,13 @@ import (
|
||||
)
|
||||
|
||||
type TokenService interface {
|
||||
GenerateAccessToken(user model.User, authenticationMethod string) (string, error)
|
||||
GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error)
|
||||
}
|
||||
|
||||
type AuditLogger interface {
|
||||
Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool)
|
||||
}
|
||||
|
||||
type AppConfigProvider interface {
|
||||
GetDbConfig() *model.AppConfig
|
||||
}
|
||||
|
||||
type UserCreator interface {
|
||||
CreateUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error)
|
||||
}
|
||||
@@ -31,7 +28,6 @@ type Dependencies struct {
|
||||
|
||||
Signer TokenService
|
||||
AuditLog AuditLogger
|
||||
AppConfig AppConfigProvider
|
||||
UserCreator UserCreator
|
||||
}
|
||||
|
||||
@@ -44,7 +40,7 @@ func New(deps Dependencies) *Module {
|
||||
service := newService(deps)
|
||||
return &Module{
|
||||
service: service,
|
||||
handler: newHandler(service, deps.AppConfig),
|
||||
handler: newHandler(service),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ package usersignup
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
@@ -25,7 +27,6 @@ type Service struct {
|
||||
userCreator UserCreator
|
||||
signer TokenService
|
||||
auditLog AuditLogger
|
||||
appConfig AppConfigProvider
|
||||
}
|
||||
|
||||
func newService(deps Dependencies) *Service {
|
||||
@@ -34,11 +35,15 @@ func newService(deps Dependencies) *Service {
|
||||
userCreator: deps.UserCreator,
|
||||
signer: deps.Signer,
|
||||
auditLog: deps.AuditLog,
|
||||
appConfig: deps.AppConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SignUp(ctx context.Context, signupData signUpDto, ipAddress, userAgent string) (model.User, string, error) {
|
||||
config, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return model.User{}, "", fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -46,8 +51,7 @@ func (s *Service) SignUp(ctx context.Context, signupData signUpDto, ipAddress, u
|
||||
|
||||
tokenProvided := signupData.Token != ""
|
||||
|
||||
config := s.appConfig.GetDbConfig()
|
||||
if config.AllowUserSignups.Value != "open" && !tokenProvided {
|
||||
if config.AllowUserSignups.String() != "open" && !tokenProvided {
|
||||
return model.User{}, "", &common.OpenSignupDisabledError{}
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ func (s *Service) SignUp(ctx context.Context, signupData signUpDto, ipAddress, u
|
||||
LastName: signupData.LastName,
|
||||
DisplayName: strings.TrimSpace(signupData.FirstName + " " + signupData.LastName),
|
||||
UserGroupIds: userGroupIDs,
|
||||
EmailVerified: s.appConfig.GetDbConfig().EmailsVerified.IsTrue(),
|
||||
EmailVerified: config.EmailsVerified.IsTrue(),
|
||||
}
|
||||
|
||||
user, err := s.userCreator.CreateUserInternal(ctx, userToCreate, false, tx)
|
||||
@@ -92,7 +96,7 @@ func (s *Service) SignUp(ctx context.Context, signupData signUpDto, ipAddress, u
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
accessToken, err := s.signer.GenerateAccessToken(user, "")
|
||||
accessToken, err := s.signer.GenerateAccessToken(user, "", config.SessionDuration.AsDurationMinutes())
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -123,6 +127,11 @@ func (s *Service) SignUp(ctx context.Context, signupData signUpDto, ipAddress, u
|
||||
}
|
||||
|
||||
func (s *Service) SignUpInitialAdmin(ctx context.Context, signUpData signUpDto) (model.User, string, error) {
|
||||
config, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return model.User{}, "", fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
@@ -150,7 +159,7 @@ func (s *Service) SignUpInitialAdmin(ctx context.Context, signUpData signUpDto)
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
token, err := s.signer.GenerateAccessToken(user, authenticationMethodOneTimePassword)
|
||||
token, err := s.signer.GenerateAccessToken(user, authenticationMethodOneTimePassword, config.SessionDuration.AsDurationMinutes())
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
service *Service
|
||||
appConfig AppConfigProvider
|
||||
service *Service
|
||||
}
|
||||
|
||||
func newHandler(service *Service) *handler {
|
||||
@@ -67,6 +68,12 @@ func (h *handler) beginLogin(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *handler) verifyLogin(c *gin.Context) {
|
||||
dbConfig, err := appconfig.FromCtx(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
|
||||
if err != nil {
|
||||
_ = c.Error(&common.MissingSessionIdError{})
|
||||
@@ -91,7 +98,7 @@ func (h *handler) verifyLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
maxAge := int(h.appConfig.GetDbConfig().SessionDuration.AsDurationMinutes().Seconds())
|
||||
maxAge := int(dbConfig.SessionDuration.AsDurationMinutes().Seconds())
|
||||
cookie.AddAccessTokenCookie(c, maxAge, token)
|
||||
|
||||
c.JSON(http.StatusOK, userDto)
|
||||
|
||||
@@ -23,16 +23,13 @@ type AuditLogger interface {
|
||||
CreateNewSignInWithEmail(ctx context.Context, ipAddress, userAgent, userID string, tx *gorm.DB, dbConfig *appconfig.AppConfigModel) model.AuditLog
|
||||
}
|
||||
|
||||
type AppConfigProvider interface {
|
||||
GetDbConfig() *model.AppConfig
|
||||
}
|
||||
|
||||
type Dependencies struct {
|
||||
DB *gorm.DB
|
||||
AppURL string
|
||||
|
||||
Signer TokenService
|
||||
AuditLog AuditLogger
|
||||
Signer TokenService
|
||||
AuditLog AuditLogger
|
||||
AppConfig *appconfig.AppConfigService
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
@@ -40,8 +37,8 @@ type Module struct {
|
||||
handler *handler
|
||||
}
|
||||
|
||||
func New(deps Dependencies) (*Module, error) {
|
||||
service, err := newService(deps)
|
||||
func New(ctx context.Context, deps Dependencies) (*Module, error) {
|
||||
service, err := newService(ctx, deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -31,9 +31,14 @@ type Service struct {
|
||||
auditLog AuditLogger
|
||||
}
|
||||
|
||||
func newService(deps Dependencies) (*Service, error) {
|
||||
func newService(ctx context.Context, deps Dependencies) (*Service, error) {
|
||||
dbConfig, err := deps.AppConfig.GetConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
wa, err := gowebauthn.New(&gowebauthn.Config{
|
||||
RPDisplayName: deps.AppConfig.GetDbConfig().AppName.Value,
|
||||
RPDisplayName: dbConfig.AppName.String(),
|
||||
RPID: utils.GetHostnameFromURL(deps.AppURL),
|
||||
RPOrigins: []string{deps.AppURL},
|
||||
AuthenticatorSelection: protocol.AuthenticatorSelection{
|
||||
@@ -65,15 +70,18 @@ func newService(deps Dependencies) (*Service, error) {
|
||||
}
|
||||
|
||||
func (s *Service) BeginRegistration(ctx context.Context, userID string) (*PublicKeyCredentialCreationOptions, error) {
|
||||
err := s.updateWebAuthnConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
s.updateWebAuthnConfig()
|
||||
|
||||
var user model.User
|
||||
err := tx.
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Preload("Credentials").
|
||||
Find(&user, "id = ?", userID).
|
||||
@@ -274,7 +282,7 @@ func (s *Service) VerifyLogin(ctx context.Context, sessionID string, credentialA
|
||||
return model.User{}, "", &common.UserDisabledError{}
|
||||
}
|
||||
|
||||
token, err := s.signer.GenerateAccessToken(*user, authenticationMethodPhishingResistant)
|
||||
token, err := s.signer.GenerateAccessToken(*user, authenticationMethodPhishingResistant, dbConfig.SessionDuration.AsDurationMinutes())
|
||||
if err != nil {
|
||||
return model.User{}, "", err
|
||||
}
|
||||
@@ -377,8 +385,14 @@ func (s *Service) UpdateCredential(ctx context.Context, userID, credentialID, na
|
||||
}
|
||||
|
||||
// updateWebAuthnConfig updates the WebAuthn configuration with the app name as it can change during runtime
|
||||
func (s *Service) updateWebAuthnConfig() {
|
||||
s.webAuthn.Config.RPDisplayName = s.appConfig.GetDbConfig().AppName.Value
|
||||
func (s *Service) updateWebAuthnConfig(ctx context.Context) error {
|
||||
dbConfig, err := appconfig.FromCtx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
s.webAuthn.Config.RPDisplayName = dbConfig.AppName.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateReauthenticationTokenWithAccessToken(ctx context.Context, accessToken string) (string, error) {
|
||||
|
||||
@@ -28,7 +28,7 @@ func newFakeSigner() *fakeSigner {
|
||||
return &fakeSigner{tokens: map[string]jwt.Token{}}
|
||||
}
|
||||
|
||||
func (s *fakeSigner) GenerateAccessToken(user model.User, authenticationMethod string) (string, error) {
|
||||
func (s *fakeSigner) GenerateAccessToken(user model.User, authenticationMethod string, _ time.Duration) (string, error) {
|
||||
builder := jwt.NewBuilder().
|
||||
Subject(user.ID).
|
||||
IssuedAt(time.Now())
|
||||
@@ -85,7 +85,7 @@ func TestCreateReauthenticationTokenWithAccessToken(t *testing.T) {
|
||||
|
||||
t.Run("accepts a fresh access token from WebAuthn login", func(t *testing.T) {
|
||||
service, signer, user := setupService(t)
|
||||
accessToken, err := signer.GenerateAccessToken(user, authenticationMethodPhishingResistant)
|
||||
accessToken, err := signer.GenerateAccessToken(user, authenticationMethodPhishingResistant, time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
@@ -96,7 +96,7 @@ func TestCreateReauthenticationTokenWithAccessToken(t *testing.T) {
|
||||
|
||||
t.Run("rejects a fresh access token from one-time access login", func(t *testing.T) {
|
||||
service, signer, user := setupService(t)
|
||||
accessToken, err := signer.GenerateAccessToken(user, "otp")
|
||||
accessToken, err := signer.GenerateAccessToken(user, "otp", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
@@ -108,7 +108,7 @@ func TestCreateReauthenticationTokenWithAccessToken(t *testing.T) {
|
||||
|
||||
t.Run("rejects a fresh access token without an authentication method", func(t *testing.T) {
|
||||
service, signer, user := setupService(t)
|
||||
accessToken, err := signer.GenerateAccessToken(user, "")
|
||||
accessToken, err := signer.GenerateAccessToken(user, "", time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
reauthenticationToken, err := service.CreateReauthenticationTokenWithAccessToken(t.Context(), accessToken)
|
||||
|
||||
Reference in New Issue
Block a user