From 38e6e8fb8dc99ebb8391e4725ad7a36517592ea9 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Wed, 6 Nov 2024 15:21:21 +0100 Subject: [PATCH 01/13] feat: return mfa only flag --- backend/dto/webauthn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/dto/webauthn.go b/backend/dto/webauthn.go index 9334677f6..9cc7d6d08 100644 --- a/backend/dto/webauthn.go +++ b/backend/dto/webauthn.go @@ -21,6 +21,7 @@ type WebauthnCredentialResponse struct { Transports []string `json:"transports"` BackupEligible bool `json:"backup_eligible"` BackupState bool `json:"backup_state"` + MFAOnly bool `json:"mfa_only"` } // FromWebauthnCredentialModel Converts the DB model to a DTO object @@ -36,5 +37,6 @@ func FromWebauthnCredentialModel(c *models.WebauthnCredential) *WebauthnCredenti Transports: c.Transports.GetNames(), BackupEligible: c.BackupEligible, BackupState: c.BackupState, + MFAOnly: c.MFAOnly, } } From 94347c7a7de27aaae9d0167dd4f7187b5808ea96 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Wed, 6 Nov 2024 15:41:31 +0100 Subject: [PATCH 02/13] feat: add webauthn admin handler --- backend/dto/admin/webauthn.go | 10 ++ backend/handler/wenauthn_credential_admin.go | 119 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 backend/dto/admin/webauthn.go create mode 100644 backend/handler/wenauthn_credential_admin.go diff --git a/backend/dto/admin/webauthn.go b/backend/dto/admin/webauthn.go new file mode 100644 index 000000000..ef6a68a03 --- /dev/null +++ b/backend/dto/admin/webauthn.go @@ -0,0 +1,10 @@ +package admin + +type ListWebauthnCredentialsRequestDto struct { + UserID string `param:"user_id" validate:"required,uuid"` +} + +type GetWebauthnCredentialRequestDto struct { + ListWebauthnCredentialsRequestDto + WebauthnCredentialID string `param:"credential_id" validate:"required"` +} diff --git a/backend/handler/wenauthn_credential_admin.go b/backend/handler/wenauthn_credential_admin.go new file mode 100644 index 000000000..37dd3eca9 --- /dev/null +++ b/backend/handler/wenauthn_credential_admin.go @@ -0,0 +1,119 @@ +package handler + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/dto" + "github.com/teamhanko/hanko/backend/dto/admin" + "github.com/teamhanko/hanko/backend/persistence" + "net/http" +) + +type WebauthnCredentialAdminHandler interface { + List(ctx echo.Context) error + Get(ctx echo.Context) error + Delete(ctx echo.Context) error +} + +type webauthnCredentialAdminHandler struct { + persister persistence.Persister +} + +func NewWebauthnCredentialAdminHandler(persister persistence.Persister) WebauthnCredentialAdminHandler { + return &webauthnCredentialAdminHandler{ + persister: persister, + } +} + +func (h *webauthnCredentialAdminHandler) List(ctx echo.Context) error { + listDto, err := loadDto2[admin.ListWebauthnCredentialsRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(listDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + credentials, err := h.persister.GetWebauthnCredentialPersister().GetFromUser(userID) + if err != nil { + return err + } + + credentialResponses := make([]dto.WebauthnCredentialResponse, len(credentials)) + for i := range credentials { + credentialResponses[i] = *dto.FromWebauthnCredentialModel(&credentials[i]) + } + + return ctx.JSON(http.StatusOK, credentialResponses) +} + +func (h *webauthnCredentialAdminHandler) Get(ctx echo.Context) error { + getDto, err := loadDto2[admin.GetWebauthnCredentialRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(getDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + credential, err := h.persister.GetWebauthnCredentialPersister().Get(getDto.WebauthnCredentialID) + if err != nil { + return err + } + + if credential == nil || credential.UserId != userID { + return echo.NewHTTPError(http.StatusNotFound, "webauthn credential not found") + } + + return ctx.JSON(http.StatusOK, dto.FromWebauthnCredentialModel(credential)) +} + +func (h *webauthnCredentialAdminHandler) Delete(ctx echo.Context) error { + deleteDto, err := loadDto2[admin.GetWebauthnCredentialRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(deleteDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + credential, err := h.persister.GetWebauthnCredentialPersister().Get(deleteDto.WebauthnCredentialID) + if err != nil { + return err + } + + if credential == nil || credential.UserId != userID { + return echo.NewHTTPError(http.StatusNotFound, "webauthn credential not found") + } + + err = h.persister.GetWebauthnCredentialPersister().Delete(*credential) + if err != nil { + return err + } + + return ctx.NoContent(http.StatusNoContent) +} + +func loadDto2[I any](ctx echo.Context) (*I, error) { + var adminDto I + err := ctx.Bind(&adminDto) + if err != nil { + ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusBadRequest, err) + } + + err = ctx.Validate(adminDto) + if err != nil { + ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusBadRequest, err) + } + + return &adminDto, nil +} From de9dc928bac2a63afe3516af2158be312c3fa3a5 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Thu, 7 Nov 2024 15:48:04 +0100 Subject: [PATCH 03/13] feat: add webauthn credential handler to router --- backend/handler/admin_router.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/handler/admin_router.go b/backend/handler/admin_router.go index 2bbd2fa87..a6ab926bc 100644 --- a/backend/handler/admin_router.go +++ b/backend/handler/admin_router.go @@ -67,6 +67,12 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh email.DELETE("/:email_id", emailHandler.Delete) email.POST("/:email_id/set_primary", emailHandler.SetPrimaryEmail) + webauthnCredentialHandler := NewWebauthnCredentialAdminHandler(persister) + webauthnCredentials := user.Group("/:user_id/webauthn_credentials") + webauthnCredentials.GET("", webauthnCredentialHandler.List) + webauthnCredentials.GET("/:credential_id", webauthnCredentialHandler.Get) + webauthnCredentials.DELETE("/:credential_id", webauthnCredentialHandler.Delete) + auditLogHandler := NewAuditLogHandler(persister) auditLogs := g.Group("/audit_logs") From 57298e2cad9efe0dbea64221d1f887f8ea44b5c6 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Mon, 11 Nov 2024 08:55:10 +0100 Subject: [PATCH 04/13] feat: add password mgmt admin endpoints --- backend/dto/admin/password.go | 9 ++ backend/flow_api/services/password.go | 5 +- backend/handler/admin_router.go | 7 ++ backend/handler/password_admin.go | 156 ++++++++++++++++++++++++++ backend/handler/public_router.go | 2 +- 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 backend/handler/password_admin.go diff --git a/backend/dto/admin/password.go b/backend/dto/admin/password.go index 3e35f65ad..7d65a31ae 100644 --- a/backend/dto/admin/password.go +++ b/backend/dto/admin/password.go @@ -10,3 +10,12 @@ type PasswordCredential struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +type GetPasswordCredentialRequestDto struct { + UserID string `param:"user_id" validate:"required,uuid4"` +} + +type CreateOrUpdatePasswordCredentialRequestDto struct { + GetPasswordCredentialRequestDto + Password string `json:"password" validate:"required"` +} diff --git a/backend/flow_api/services/password.go b/backend/flow_api/services/password.go index 3e668c7d5..67fe9c40c 100644 --- a/backend/flow_api/services/password.go +++ b/backend/flow_api/services/password.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" - "github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/persistence" "github.com/teamhanko/hanko/backend/persistence/models" "golang.org/x/crypto/bcrypt" @@ -25,13 +24,11 @@ type Password interface { type password struct { persister persistence.Persister - cfg config.Config } -func NewPasswordService(cfg config.Config, persister persistence.Persister) Password { +func NewPasswordService(persister persistence.Persister) Password { return &password{ persister, - cfg, } } diff --git a/backend/handler/admin_router.go b/backend/handler/admin_router.go index a6ab926bc..d6a3aa85c 100644 --- a/backend/handler/admin_router.go +++ b/backend/handler/admin_router.go @@ -73,6 +73,13 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh webauthnCredentials.GET("/:credential_id", webauthnCredentialHandler.Get) webauthnCredentials.DELETE("/:credential_id", webauthnCredentialHandler.Delete) + passwordCredentialHandler := NewPasswordAdminHandler(persister) + passwordCredentials := user.Group("/:user_id/passwords") + passwordCredentials.GET("", passwordCredentialHandler.Get) + passwordCredentials.POST("", passwordCredentialHandler.Create) + passwordCredentials.PUT("", passwordCredentialHandler.Update) + passwordCredentials.DELETE("", passwordCredentialHandler.Delete) + auditLogHandler := NewAuditLogHandler(persister) auditLogs := g.Group("/audit_logs") diff --git a/backend/handler/password_admin.go b/backend/handler/password_admin.go new file mode 100644 index 000000000..25303a596 --- /dev/null +++ b/backend/handler/password_admin.go @@ -0,0 +1,156 @@ +package handler + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/dto/admin" + "github.com/teamhanko/hanko/backend/flow_api/services" + "github.com/teamhanko/hanko/backend/persistence" + "net/http" +) + +type PasswordAdminHandler interface { + Get(ctx echo.Context) error + Create(ctx echo.Context) error + Update(ctx echo.Context) error + Delete(ctx echo.Context) error +} + +type passwordAdminHandler struct { + persister persistence.Persister + passwordService services.Password +} + +func NewPasswordAdminHandler(persister persistence.Persister) PasswordAdminHandler { + return &passwordAdminHandler{ + persister: persister, + passwordService: services.NewPasswordService(persister), + } +} + +func (h *passwordAdminHandler) Get(ctx echo.Context) error { + getDto, err := loadDto2[admin.GetPasswordCredentialRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(getDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) + if err != nil { + return err + } + + if credential == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + dto := admin.PasswordCredential{ + ID: credential.ID, + CreatedAt: credential.CreatedAt, + UpdatedAt: credential.UpdatedAt, + } + + return ctx.JSON(http.StatusOK, dto) +} + +func (h *passwordAdminHandler) Create(ctx echo.Context) error { + createDto, err := loadDto2[admin.CreateOrUpdatePasswordCredentialRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(createDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + err = h.passwordService.CreatePassword(h.persister.GetConnection(), userID, createDto.Password) + if err != nil { + return err + } + + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) + if err != nil { + return err + } + + dto := admin.PasswordCredential{ + ID: credential.ID, + CreatedAt: credential.CreatedAt, + UpdatedAt: credential.UpdatedAt, + } + + return ctx.JSON(http.StatusOK, dto) +} + +func (h *passwordAdminHandler) Update(ctx echo.Context) error { + updateDto, err := loadDto2[admin.CreateOrUpdatePasswordCredentialRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(updateDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) + if err != nil { + return err + } + + if credential == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + err = h.passwordService.UpdatePassword(h.persister.GetConnection(), credential, updateDto.Password) + if err != nil { + return err + } + + credential, err = h.persister.GetPasswordCredentialPersister().GetByUserID(userID) + if err != nil { + return err + } + + dto := admin.PasswordCredential{ + ID: credential.ID, + CreatedAt: credential.CreatedAt, + UpdatedAt: credential.UpdatedAt, + } + + return ctx.JSON(http.StatusOK, dto) +} + +func (h *passwordAdminHandler) Delete(ctx echo.Context) error { + getDto, err := loadDto2[admin.GetPasswordCredentialRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(getDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) + if err != nil { + return err + } + + if credential == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + err = h.persister.GetPasswordCredentialPersister().Delete(*credential) + if err != nil { + return err + } + + return ctx.NoContent(http.StatusNoContent) +} diff --git a/backend/handler/public_router.go b/backend/handler/public_router.go index 88d5dcdb8..7e5c1b8e4 100644 --- a/backend/handler/public_router.go +++ b/backend/handler/public_router.go @@ -31,7 +31,7 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet emailService, err := services.NewEmailService(*cfg) passcodeService := services.NewPasscodeService(*cfg, *emailService, persister) - passwordService := services.NewPasswordService(*cfg, persister) + passwordService := services.NewPasswordService(persister) webauthnService := services.NewWebauthnService(*cfg, persister) jwkManager, err := jwk.NewDefaultManager(cfg.Secrets.Keys, persister.GetJwkPersister()) From 58265ec02b2a6dec0efb4542e71b32f5e1d5d0f7 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Tue, 12 Nov 2024 16:56:55 +0100 Subject: [PATCH 05/13] feat: add sessions admin handler --- backend/dto/admin/session.go | 9 +++++ backend/handler/admin_router.go | 6 ++- backend/handler/session_admin.go | 55 +++++++++++++++++++++++++++ backend/persistence/models/session.go | 16 ++++---- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/backend/dto/admin/session.go b/backend/dto/admin/session.go index e9c7c4133..d9e7902c4 100644 --- a/backend/dto/admin/session.go +++ b/backend/dto/admin/session.go @@ -9,3 +9,12 @@ type CreateSessionTokenDto struct { type CreateSessionTokenResponse struct { SessionToken string `json:"session_token"` } + +type ListSessionsRequestDto struct { + UserID string `param:"user_id" validate:"required,uuid4"` +} + +type DeleteSessionRequestDto struct { + ListSessionsRequestDto + SessionID string `param:"session_id" validate:"required,uuid4"` +} diff --git a/backend/handler/admin_router.go b/backend/handler/admin_router.go index ec3e1b313..5cf3655be 100644 --- a/backend/handler/admin_router.go +++ b/backend/handler/admin_router.go @@ -60,6 +60,7 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh userHandler := NewUserHandlerAdmin(persister) emailHandler := NewEmailAdminHandler(cfg, persister) + sessionsHandler := NewSessionAdminHandler(cfg, persister, sessionManager, auditLogger) user := g.Group("/users") user.GET("", userHandler.List) @@ -87,6 +88,10 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh passwordCredentials.PUT("", passwordCredentialHandler.Update) passwordCredentials.DELETE("", passwordCredentialHandler.Delete) + userSessions := user.Group("/:user_id/sessions") + userSessions.GET("", sessionsHandler.List) + userSessions.DELETE("/:session_id", sessionsHandler.Delete) + auditLogHandler := NewAuditLogHandler(persister) auditLogs := g.Group("/audit_logs") @@ -100,7 +105,6 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh webhooks.DELETE("/:id", webhookHandler.Delete) webhooks.PUT("/:id", webhookHandler.Update) - sessionsHandler := NewSessionAdminHandler(cfg, persister, sessionManager, auditLogger) sessions := g.Group("/sessions") sessions.POST("", sessionsHandler.Generate) diff --git a/backend/handler/session_admin.go b/backend/handler/session_admin.go index 3e1f75ada..03bfa1744 100644 --- a/backend/handler/session_admin.go +++ b/backend/handler/session_admin.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gofrs/uuid" "github.com/labstack/echo/v4" + "github.com/pkg/errors" auditlog "github.com/teamhanko/hanko/backend/audit_log" "github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/dto" @@ -111,3 +112,57 @@ func (h *SessionAdminHandler) Generate(ctx echo.Context) error { return ctx.JSON(http.StatusOK, response) } + +func (h *SessionAdminHandler) List(ctx echo.Context) error { + listDto, err := loadDto2[admin.ListSessionsRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(listDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + sessions, err := h.persister.GetSessionPersister().ListActive(userID) + if err != nil { + return err + } + + return ctx.JSON(http.StatusOK, sessions) +} + +func (h *SessionAdminHandler) Delete(ctx echo.Context) error { + deleteDto, err := loadDto2[admin.DeleteSessionRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(deleteDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + sessionID, err := uuid.FromString(deleteDto.SessionID) + if err != nil { + return fmt.Errorf("failed to pasre session_id as uuid: %s", err) + } + + sessionModel, err := h.persister.GetSessionPersister().Get(sessionID) + if err != nil { + return err + } + + if sessionModel == nil { + return echo.NewHTTPError(http.StatusNotFound) + } else if sessionModel.UserID != userID { + return echo.NewHTTPError(http.StatusNotFound).SetInternal(errors.New("session does not belong to user")) + } + + err = h.persister.GetSessionPersister().Delete(*sessionModel) + if err != nil { + return err + } + + return ctx.NoContent(http.StatusNoContent) +} diff --git a/backend/persistence/models/session.go b/backend/persistence/models/session.go index 6dfe7df2c..d0d6f6597 100644 --- a/backend/persistence/models/session.go +++ b/backend/persistence/models/session.go @@ -9,14 +9,14 @@ import ( ) type Session struct { - ID uuid.UUID `db:"id"` - UserID uuid.UUID `db:"user_id"` - UserAgent string `db:"user_agent"` - IpAddress string `db:"ip_address"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - ExpiresAt *time.Time `db:"expires_at"` - LastUsed time.Time `db:"last_used"` + ID uuid.UUID `db:"id" json:"id"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + UserAgent string `db:"user_agent" json:"user_agent"` + IpAddress string `db:"ip_address" json:"ip_address"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ExpiresAt *time.Time `db:"expires_at" json:"expires_at"` + LastUsed time.Time `db:"last_used" json:"last_used"` } func (session *Session) Validate(tx *pop.Connection) (*validate.Errors, error) { From 5b745a9e8d03d0bf62ea53c1f5801e9ebbce63d1 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Wed, 13 Nov 2024 11:30:56 +0100 Subject: [PATCH 06/13] feat: add otp admin handler --- backend/dto/admin/otp.go | 15 +++++++ backend/handler/admin_router.go | 5 +++ backend/handler/otp_admin.go | 77 +++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 backend/dto/admin/otp.go create mode 100644 backend/handler/otp_admin.go diff --git a/backend/dto/admin/otp.go b/backend/dto/admin/otp.go new file mode 100644 index 000000000..158c7bd87 --- /dev/null +++ b/backend/dto/admin/otp.go @@ -0,0 +1,15 @@ +package admin + +import ( + "github.com/gofrs/uuid" + "time" +) + +type GetOTPRequestDto struct { + UserID string `param:"user_id" validate:"required,uuid4"` +} + +type OTPDto struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/backend/handler/admin_router.go b/backend/handler/admin_router.go index 5cf3655be..772f32240 100644 --- a/backend/handler/admin_router.go +++ b/backend/handler/admin_router.go @@ -92,6 +92,11 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh userSessions.GET("", sessionsHandler.List) userSessions.DELETE("/:session_id", sessionsHandler.Delete) + otpHandler := NewOTPAdminHandler(persister) + otp := user.Group("/:user_id/otp") + otp.GET("", otpHandler.Get) + otp.DELETE("", otpHandler.Delete) + auditLogHandler := NewAuditLogHandler(persister) auditLogs := g.Group("/audit_logs") diff --git a/backend/handler/otp_admin.go b/backend/handler/otp_admin.go new file mode 100644 index 000000000..52be2cd7c --- /dev/null +++ b/backend/handler/otp_admin.go @@ -0,0 +1,77 @@ +package handler + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/hanko/backend/dto/admin" + "github.com/teamhanko/hanko/backend/persistence" + "net/http" +) + +type OTPAdminHandler interface { + Get(ctx echo.Context) error + Delete(ctx echo.Context) error +} + +type otpAdminHandler struct { + persister persistence.Persister +} + +func NewOTPAdminHandler(persister persistence.Persister) OTPAdminHandler { + return &otpAdminHandler{persister: persister} +} + +func (h *otpAdminHandler) Get(ctx echo.Context) error { + getDto, err := loadDto2[admin.GetOTPRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(getDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + userModel, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if userModel == nil || userModel.OTPSecret == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + return ctx.JSON(http.StatusOK, admin.OTPDto{ + ID: userModel.OTPSecret.ID, + CreatedAt: userModel.OTPSecret.CreatedAt, + }) +} + +func (h *otpAdminHandler) Delete(ctx echo.Context) error { + deleteDto, err := loadDto2[admin.GetOTPRequestDto](ctx) + if err != nil { + return err + } + + userID, err := uuid.FromString(deleteDto.UserID) + if err != nil { + return fmt.Errorf(parseUserUuidFailureMessage, err) + } + + userModel, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if userModel == nil || userModel.OTPSecret == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + err = h.persister.GetOTPSecretPersister().Delete(userModel.OTPSecret) + if err != nil { + return err + } + + return ctx.NoContent(http.StatusNoContent) +} From 988ee40d96b8e880e0bdacd7e73fac680a02c840 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Wed, 13 Nov 2024 11:47:48 +0100 Subject: [PATCH 07/13] feat: add otp to admin user dto --- backend/dto/admin/user.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/dto/admin/user.go b/backend/dto/admin/user.go index d477add79..61de32ee9 100644 --- a/backend/dto/admin/user.go +++ b/backend/dto/admin/user.go @@ -16,6 +16,7 @@ type User struct { UpdatedAt time.Time `json:"updated_at"` Password *PasswordCredential `json:"password,omitempty"` Identities []Identity `json:"identities,omitempty"` + OTP *OTPDto `json:"otp"` } // FromUserModel Converts the DB model to a DTO object @@ -46,6 +47,14 @@ func FromUserModel(model models.User) User { } } + var otp *OTPDto = nil + if model.OTPSecret != nil { + otp = &OTPDto{ + ID: model.OTPSecret.ID, + CreatedAt: model.OTPSecret.CreatedAt, + } + } + return User{ ID: model.ID, WebauthnCredentials: credentials, @@ -55,6 +64,7 @@ func FromUserModel(model models.User) User { UpdatedAt: model.UpdatedAt, Password: passwordCredential, Identities: identities, + OTP: otp, } } From ca97cd255dcdd8d9276d8609442b2c1b1664d190 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Wed, 13 Nov 2024 15:45:55 +0100 Subject: [PATCH 08/13] test: add admin password handler test --- backend/handler/admin_router.go | 2 +- backend/handler/password_admin.go | 45 ++++ backend/handler/password_admin_test.go | 319 +++++++++++++++++++++++++ 3 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 backend/handler/password_admin_test.go diff --git a/backend/handler/admin_router.go b/backend/handler/admin_router.go index 772f32240..82a494cdb 100644 --- a/backend/handler/admin_router.go +++ b/backend/handler/admin_router.go @@ -82,7 +82,7 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh webauthnCredentials.DELETE("/:credential_id", webauthnCredentialHandler.Delete) passwordCredentialHandler := NewPasswordAdminHandler(persister) - passwordCredentials := user.Group("/:user_id/passwords") + passwordCredentials := user.Group("/:user_id/password") passwordCredentials.GET("", passwordCredentialHandler.Get) passwordCredentials.POST("", passwordCredentialHandler.Create) passwordCredentials.PUT("", passwordCredentialHandler.Update) diff --git a/backend/handler/password_admin.go b/backend/handler/password_admin.go index 25303a596..10a1c5211 100644 --- a/backend/handler/password_admin.go +++ b/backend/handler/password_admin.go @@ -40,6 +40,15 @@ func (h *passwordAdminHandler) Get(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) if err != nil { return err @@ -69,6 +78,24 @@ func (h *passwordAdminHandler) Create(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + + existingCredential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) + if err != nil { + return err + } + + if existingCredential != nil { + return echo.NewHTTPError(http.StatusConflict) + } + err = h.passwordService.CreatePassword(h.persister.GetConnection(), userID, createDto.Password) if err != nil { return err @@ -99,6 +126,15 @@ func (h *passwordAdminHandler) Update(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) if err != nil { return err @@ -138,6 +174,15 @@ func (h *passwordAdminHandler) Delete(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + credential, err := h.persister.GetPasswordCredentialPersister().GetByUserID(userID) if err != nil { return err diff --git a/backend/handler/password_admin_test.go b/backend/handler/password_admin_test.go new file mode 100644 index 000000000..4d3088df4 --- /dev/null +++ b/backend/handler/password_admin_test.go @@ -0,0 +1,319 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/dto/admin" + "github.com/teamhanko/hanko/backend/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPasswordAdminSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(passwordAdminSuite)) +} + +type passwordAdminSuite struct { + test.Suite +} + +func (s *passwordAdminSuite) TestPasswordAdminHandler_Get() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/password") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userId string + expectedStatusCode int + }{ + { + name: "should return password credential", + userId: "38bf5a00-d7ea-40a5-a5de-48722c148925", + }, + { + name: "should fail if user has no password", + userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userId: "customUserId", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userId: "", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userId: "30f41697-b413-43cc-8cca-d55298683607", + expectedStatusCode: http.StatusNotFound, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s/password", currentTest.userId), nil) + + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + if http.StatusOK == rec.Code { + var passwordCredential *admin.PasswordCredential + s.NoError(json.Unmarshal(rec.Body.Bytes(), &passwordCredential)) + s.NotNil(passwordCredential) + } else { + s.Require().Equal(currentTest.expectedStatusCode, rec.Code) + } + }) + } +} + +func (s *passwordAdminSuite) TestPasswordAdminHandler_Create() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/password") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userId string + password string + expectedStatusCode int + }{ + { + name: "should create a new password credential", + userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", + password: "superSecure", + }, + { + name: "should fail if user already has a password", + userId: "38bf5a00-d7ea-40a5-a5de-48722c148925", + password: "superSecure", + expectedStatusCode: http.StatusConflict, + }, + { + name: "should fail on non uuid userID", + userId: "customUserId", + password: "superSecure", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userId: "", + password: "superSecure", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userId: "30f41697-b413-43cc-8cca-d55298683607", + password: "superSecure", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on empty password", + userId: "30f41697-b413-43cc-8cca-d55298683607", + password: "", + expectedStatusCode: http.StatusBadRequest, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + testDto := admin.CreateOrUpdatePasswordCredentialRequestDto{ + GetPasswordCredentialRequestDto: admin.GetPasswordCredentialRequestDto{ + UserID: currentTest.userId, + }, + Password: currentTest.password, + } + + testJson, err := json.Marshal(testDto) + s.Require().NoError(err) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/users/%s/password", currentTest.userId), bytes.NewReader(testJson)) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + if http.StatusOK == rec.Code { + var passwordCredential *admin.PasswordCredential + s.NoError(json.Unmarshal(rec.Body.Bytes(), &passwordCredential)) + s.NotNil(passwordCredential) + + cred, err := s.Storage.GetPasswordCredentialPersister().GetByUserID(uuid.FromStringOrNil(currentTest.userId)) + s.Require().NoError(err) + s.Require().NotNil(cred) + } else { + s.Require().Equal(currentTest.expectedStatusCode, rec.Code) + } + }) + } +} + +func (s *passwordAdminSuite) TestPasswordAdminHandler_Update() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/password") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userId string + oldHashedPassword string + password string + expectedStatusCode int + }{ + { + name: "should update a password credential", + userId: "38bf5a00-d7ea-40a5-a5de-48722c148925", + oldHashedPassword: "$2a$12$Cf7k.dG6pznTUJ5u2u1pgu6I4VXH5.9O0NZsDk8TwWwyBkZovYVli", + password: "superSecure", + }, + { + name: "should fail if user already has no password", + userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", + password: "superSecure", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userId: "customUserId", + password: "superSecure", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userId: "", + password: "superSecure", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userId: "30f41697-b413-43cc-8cca-d55298683607", + password: "superSecure", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on empty password", + userId: "30f41697-b413-43cc-8cca-d55298683607", + password: "", + expectedStatusCode: http.StatusBadRequest, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + testDto := admin.CreateOrUpdatePasswordCredentialRequestDto{ + GetPasswordCredentialRequestDto: admin.GetPasswordCredentialRequestDto{ + UserID: currentTest.userId, + }, + Password: currentTest.password, + } + + testJson, err := json.Marshal(testDto) + s.Require().NoError(err) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/users/%s/password", currentTest.userId), bytes.NewReader(testJson)) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + if http.StatusOK == rec.Code { + var passwordCredential *admin.PasswordCredential + s.NoError(json.Unmarshal(rec.Body.Bytes(), &passwordCredential)) + s.NotNil(passwordCredential) + + cred, err := s.Storage.GetPasswordCredentialPersister().GetByUserID(uuid.FromStringOrNil(currentTest.userId)) + s.Require().NoError(err) + s.NotEqual(currentTest.oldHashedPassword, cred.Password) + } else { + s.Require().Equal(currentTest.expectedStatusCode, rec.Code) + } + }) + } +} + +func (s *passwordAdminSuite) TestPasswordAdminHandler_Delete() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/password") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userId string + expectedStatusCode int + }{ + { + name: "should delete a password credential", + userId: "38bf5a00-d7ea-40a5-a5de-48722c148925", + expectedStatusCode: http.StatusNoContent, + }, + { + name: "should fail if user already has no password", + userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userId: "customUserId", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userId: "", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userId: "30f41697-b413-43cc-8cca-d55298683607", + expectedStatusCode: http.StatusNotFound, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/users/%s/password", currentTest.userId), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Require().Equal(currentTest.expectedStatusCode, rec.Code) + + if http.StatusNoContent == rec.Code { + cred, err := s.Storage.GetPasswordCredentialPersister().GetByUserID(uuid.FromStringOrNil(currentTest.userId)) + s.Require().NoError(err) + s.Require().Nil(cred) + } + }) + } +} From 8681d94823149ea7f28382f2b991abf7ea0a809b Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Thu, 14 Nov 2024 13:56:11 +0100 Subject: [PATCH 09/13] test: add admin webauthn handler test --- ..._admin.go => webauthn_credential_admin.go} | 27 ++ .../handler/webauthn_credential_admin_test.go | 256 ++++++++++++++++++ 2 files changed, 283 insertions(+) rename backend/handler/{wenauthn_credential_admin.go => webauthn_credential_admin.go} (86%) create mode 100644 backend/handler/webauthn_credential_admin_test.go diff --git a/backend/handler/wenauthn_credential_admin.go b/backend/handler/webauthn_credential_admin.go similarity index 86% rename from backend/handler/wenauthn_credential_admin.go rename to backend/handler/webauthn_credential_admin.go index 37dd3eca9..086406c29 100644 --- a/backend/handler/wenauthn_credential_admin.go +++ b/backend/handler/webauthn_credential_admin.go @@ -37,6 +37,15 @@ func (h *webauthnCredentialAdminHandler) List(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + credentials, err := h.persister.GetWebauthnCredentialPersister().GetFromUser(userID) if err != nil { return err @@ -61,6 +70,15 @@ func (h *webauthnCredentialAdminHandler) Get(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + credential, err := h.persister.GetWebauthnCredentialPersister().Get(getDto.WebauthnCredentialID) if err != nil { return err @@ -84,6 +102,15 @@ func (h *webauthnCredentialAdminHandler) Delete(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + credential, err := h.persister.GetWebauthnCredentialPersister().Get(deleteDto.WebauthnCredentialID) if err != nil { return err diff --git a/backend/handler/webauthn_credential_admin_test.go b/backend/handler/webauthn_credential_admin_test.go new file mode 100644 index 000000000..829296861 --- /dev/null +++ b/backend/handler/webauthn_credential_admin_test.go @@ -0,0 +1,256 @@ +package handler + +import ( + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/dto" + "github.com/teamhanko/hanko/backend/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestWebauthnCredentialAdminSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(webauthnCredentialAdminSuite)) +} + +type webauthnCredentialAdminSuite struct { + test.Suite +} + +func (s *webauthnCredentialAdminSuite) TestWebauthnCredentialAdminHandler_List() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/webauthn") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userID string + expectedCount int + expectedStatusCode int + }{ + { + name: "should return webauthn credentials for user with multiple credentials", + userID: "ec4ef049-5b88-4321-a173-21b0eff06a04", + expectedCount: 2, + expectedStatusCode: http.StatusOK, + }, + { + name: "should return webauthn credentials for user with one credentials", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + expectedCount: 1, + expectedStatusCode: http.StatusOK, + }, + { + name: "should return webauthn credentials for user with no credentials", + userID: "38bf5a00-d7ea-40a5-a5de-48722c148925", + expectedCount: 0, + expectedStatusCode: http.StatusOK, + }, + { + name: "should fail on non uuid userID", + userID: "customUserId", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userID: "", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userID: "30f41697-b413-43cc-8cca-d55298683607", + expectedStatusCode: http.StatusNotFound, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s/webauthn_credentials", currentTest.userID), nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Equal(currentTest.expectedStatusCode, rec.Code) + if http.StatusOK == rec.Code { + var credentials []dto.WebauthnCredentialResponse + err = json.Unmarshal(rec.Body.Bytes(), &credentials) + s.Require().NoError(err) + + s.Equal(len(credentials), currentTest.expectedCount) + } + }) + } +} + +func (s *webauthnCredentialAdminSuite) TestWebauthnCredentialAdminHandler_Get() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/webauthn") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userID string + credentialID string + expectedStatusCode int + }{ + { + name: "should return webauthn credential", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusOK, + }, + { + name: "should fail if credential is not found", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "notSoRandomCredentialID", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail for if credential is not associated to the user", + userID: "ec4ef049-5b88-4321-a173-21b0eff06a04", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non existing user", + userID: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a6", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on empty userID", + userID: "", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty credentialID", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userID: "customUserId", + credentialID: "46626836-f2db-4ec0-8752-858b544cbc78", + expectedStatusCode: http.StatusBadRequest, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s/webauthn_credentials/%s", currentTest.userID, currentTest.credentialID), nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Equal(currentTest.expectedStatusCode, rec.Code) + if http.StatusOK == rec.Code { + var credential dto.WebauthnCredentialResponse + err = json.Unmarshal(rec.Body.Bytes(), &credential) + s.Require().NoError(err) + s.Equal(currentTest.credentialID, credential.ID) + } + }) + } +} + +func (s *webauthnCredentialAdminSuite) TestWebauthnCredentialAdminHandler_Delete() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/webauthn") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userID string + credentialID string + expectedCount int + expectedStatusCode int + }{ + { + name: "should delete webauthn credential for user with multiple credentials", + userID: "ec4ef049-5b88-4321-a173-21b0eff06a04", + credentialID: "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH", + expectedCount: 1, + expectedStatusCode: http.StatusNoContent, + }, + { + name: "should delete webauthn credential for user with one credential", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedCount: 0, + expectedStatusCode: http.StatusNoContent, + }, + { + name: "should fail if credential is not found", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "notSoRandomCredentialID", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail for if credential is not associated to the user", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjK", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non existing user", + userID: "30f41697-b413-43cc-8cca-d55298683607", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on empty userID", + userID: "", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty credentialID", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + credentialID: "", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userID: "customUserId", + credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", + expectedStatusCode: http.StatusBadRequest, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/users/%s/webauthn_credentials/%s", currentTest.userID, currentTest.credentialID), nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Equal(currentTest.expectedStatusCode, rec.Code) + if http.StatusNoContent == rec.Code { + credentials, err := s.Storage.GetWebauthnCredentialPersister().GetFromUser(uuid.FromStringOrNil(currentTest.userID)) + s.Require().NoError(err) + s.Equal(currentTest.expectedCount, len(credentials)) + } + }) + } +} From e0193341d1d14cd10696092ecdab257898a04286 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Fri, 15 Nov 2024 11:55:17 +0100 Subject: [PATCH 10/13] test: add admin session handler test --- backend/handler/session_admin.go | 20 +- backend/handler/session_admin_test.go | 181 ++++++++++++++++++ backend/test/fixtures/sessions/emails.yaml | 18 ++ .../fixtures/sessions/primary_emails.yaml | 10 + backend/test/fixtures/sessions/sessions.yaml | 24 +++ backend/test/fixtures/sessions/users.yaml | 9 + 6 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 backend/handler/session_admin_test.go create mode 100644 backend/test/fixtures/sessions/emails.yaml create mode 100644 backend/test/fixtures/sessions/primary_emails.yaml create mode 100644 backend/test/fixtures/sessions/sessions.yaml create mode 100644 backend/test/fixtures/sessions/users.yaml diff --git a/backend/handler/session_admin.go b/backend/handler/session_admin.go index 03bfa1744..bf36deb4b 100644 --- a/backend/handler/session_admin.go +++ b/backend/handler/session_admin.go @@ -124,6 +124,15 @@ func (h *SessionAdminHandler) List(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + sessions, err := h.persister.GetSessionPersister().ListActive(userID) if err != nil { return err @@ -143,9 +152,18 @@ func (h *SessionAdminHandler) Delete(ctx echo.Context) error { return fmt.Errorf(parseUserUuidFailureMessage, err) } + user, err := h.persister.GetUserPersister().Get(userID) + if err != nil { + return err + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound) + } + sessionID, err := uuid.FromString(deleteDto.SessionID) if err != nil { - return fmt.Errorf("failed to pasre session_id as uuid: %s", err) + return fmt.Errorf("failed to parse session_id as uuid: %s", err) } sessionModel, err := h.persister.GetSessionPersister().Get(sessionID) diff --git a/backend/handler/session_admin_test.go b/backend/handler/session_admin_test.go new file mode 100644 index 000000000..9ed6f833a --- /dev/null +++ b/backend/handler/session_admin_test.go @@ -0,0 +1,181 @@ +package handler + +import ( + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/dto/admin" + "github.com/teamhanko/hanko/backend/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSessionAdminSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(sessionAdminSuite)) +} + +type sessionAdminSuite struct { + test.Suite +} + +func (s *sessionAdminSuite) TestSessionAdminHandler_List() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/sessions") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userID string + expectedStatusCode int + expectedCount int + }{ + { + name: "should return a list of sessions with multiple entries", + userID: "ec4ef049-5b88-4321-a173-21b0eff06a04", + expectedStatusCode: http.StatusOK, + expectedCount: 2, + }, + { + name: "should return a list of sessions with one entries", + userID: "38bf5a00-d7ea-40a5-a5de-48722c148925", + expectedStatusCode: http.StatusOK, + expectedCount: 1, + }, + { + name: "should return an empty list", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + expectedStatusCode: http.StatusOK, + expectedCount: 0, + }, + { + name: "should fail on non uuid userID", + userID: "customUserId", + expectedStatusCode: http.StatusBadRequest, + expectedCount: 0, + }, + { + name: "should fail on empty userID", + userID: "", + expectedStatusCode: http.StatusBadRequest, + expectedCount: 0, + }, + { + name: "should fail on non existing user", + userID: "30f41697-b413-43cc-8cca-d55298683607", + expectedStatusCode: http.StatusNotFound, + expectedCount: 0, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s/sessions", currentTest.userID), nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Equal(currentTest.expectedStatusCode, rec.Code) + if http.StatusOK == rec.Code { + var sessions []admin.ListSessionsRequestDto + err = json.Unmarshal(rec.Body.Bytes(), &sessions) + s.Require().NoError(err) + + s.Equal(currentTest.expectedCount, len(sessions)) + } + }) + } +} + +func (s *sessionAdminSuite) TestSessionAdminHandler_Delete() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/sessions") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userID string + sessionID string + expectedStatusCode int + expectedCount int + }{ + { + name: "should delete session for user with multiple sessions", + userID: "ec4ef049-5b88-4321-a173-21b0eff06a04", + sessionID: "d8d6dc27-fcf9-4a5c-bb50-a7a03067d936", + expectedCount: 1, + expectedStatusCode: http.StatusNoContent, + }, + { + name: "should delete session for user with one session", + userID: "38bf5a00-d7ea-40a5-a5de-48722c148925", + sessionID: "108f3789-a795-43bd-a58f-ac8e80a213cd", + expectedCount: 0, + expectedStatusCode: http.StatusNoContent, + }, + { + name: "should fail if session is not found", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + sessionID: "649c95d7-9840-4e6d-be00-6c6b93c9e885", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail for if credential is not associated to the user", + userID: "38bf5a00-d7ea-40a5-a5de-48722c148925", + sessionID: "74ba812a-923a-43e4-8020-9535dcadc0a8", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non existing user", + userID: "30f41697-b413-43cc-8cca-d55298683607", + sessionID: "6e405e60-f70c-4b8a-b0d5-8ba05dd3e793", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on empty userID", + userID: "", + sessionID: "6e405e60-f70c-4b8a-b0d5-8ba05dd3e793", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty credentialID", + userID: "46626836-f2db-4ec0-8752-858b544cbc78", + sessionID: "", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userID: "customUserId", + sessionID: "d8d6dc27-fcf9-4a5c-bb50-a7a03067d936", + expectedStatusCode: http.StatusBadRequest, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/users/%s/sessions/%s", currentTest.userID, currentTest.sessionID), nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Equal(currentTest.expectedStatusCode, rec.Code) + if http.StatusNoContent == rec.Code { + credentials, err := s.Storage.GetSessionPersister().ListActive(uuid.FromStringOrNil(currentTest.userID)) + s.Require().NoError(err) + s.Equal(currentTest.expectedCount, len(credentials)) + } + }) + } +} diff --git a/backend/test/fixtures/sessions/emails.yaml b/backend/test/fixtures/sessions/emails.yaml new file mode 100644 index 000000000..d70f16a1b --- /dev/null +++ b/backend/test/fixtures/sessions/emails.yaml @@ -0,0 +1,18 @@ +- id: 51b7c175-ceb6-45ba-aae6-0092221c1b84 + user_id: ec4ef049-5b88-4321-a173-21b0eff06a04 + address: john.doe@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + user_id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + address: jane.doe@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 1215f04b-0fad-4c54-899e-a4f7230fcc63 + user_id: 46626836-f2db-4ec0-8752-858b544cbc78 + address: test@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 diff --git a/backend/test/fixtures/sessions/primary_emails.yaml b/backend/test/fixtures/sessions/primary_emails.yaml new file mode 100644 index 000000000..e7dbac2bb --- /dev/null +++ b/backend/test/fixtures/sessions/primary_emails.yaml @@ -0,0 +1,10 @@ +- id: 8eaaa61b-ad65-45ac-a5b8-d7c6d301d29e + email_id: 51b7c175-ceb6-45ba-aae6-0092221c1b84 + user_id: ec4ef049-5b88-4321-a173-21b0eff06a04 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: df8b4c07-f97e-48aa-8895-6c8e54d5b749 + email_id: 1215f04b-0fad-4c54-899e-a4f7230fcc63 + user_id: 46626836-f2db-4ec0-8752-858b544cbc78 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 diff --git a/backend/test/fixtures/sessions/sessions.yaml b/backend/test/fixtures/sessions/sessions.yaml new file mode 100644 index 000000000..ad8d459a4 --- /dev/null +++ b/backend/test/fixtures/sessions/sessions.yaml @@ -0,0 +1,24 @@ +- id: d8d6dc27-fcf9-4a5c-bb50-a7a03067d936 + user_id: ec4ef049-5b88-4321-a173-21b0eff06a04 + user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 + ip_address: 192.168.0.1 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 + expires_at: 2100-12-31 23:59:59 + last_used: 2020-12-31 23:59:59 +- id: 74ba812a-923a-43e4-8020-9535dcadc0a8 + user_id: ec4ef049-5b88-4321-a173-21b0eff06a04 + user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 + ip_address: 192.168.0.1 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 + expires_at: 2100-12-31 23:59:59 + last_used: 2020-12-31 23:59:59 +- id: 108f3789-a795-43bd-a58f-ac8e80a213cd + user_id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 + ip_address: 192.168.0.1 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 + expires_at: 2100-12-31 23:59:59 + last_used: 2020-12-31 23:59:59 diff --git a/backend/test/fixtures/sessions/users.yaml b/backend/test/fixtures/sessions/users.yaml new file mode 100644 index 000000000..1dcef2904 --- /dev/null +++ b/backend/test/fixtures/sessions/users.yaml @@ -0,0 +1,9 @@ +- id: ec4ef049-5b88-4321-a173-21b0eff06a04 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 46626836-f2db-4ec0-8752-858b544cbc78 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 From 5f23c7e8f8188d108200cd15d8f6d6b3ab84657f Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Fri, 15 Nov 2024 12:10:23 +0100 Subject: [PATCH 11/13] test: add admin otp handler test --- backend/handler/otp_admin_test.go | 145 ++++++++++++++++++ backend/test/fixtures/otp/emails.yaml | 36 +++++ backend/test/fixtures/otp/otp_secrets.yaml | 5 + backend/test/fixtures/otp/primary_emails.yaml | 10 ++ backend/test/fixtures/otp/users.yaml | 13 ++ 5 files changed, 209 insertions(+) create mode 100644 backend/handler/otp_admin_test.go create mode 100644 backend/test/fixtures/otp/emails.yaml create mode 100644 backend/test/fixtures/otp/otp_secrets.yaml create mode 100644 backend/test/fixtures/otp/primary_emails.yaml create mode 100644 backend/test/fixtures/otp/users.yaml diff --git a/backend/handler/otp_admin_test.go b/backend/handler/otp_admin_test.go new file mode 100644 index 000000000..95d682eeb --- /dev/null +++ b/backend/handler/otp_admin_test.go @@ -0,0 +1,145 @@ +package handler + +import ( + "encoding/json" + "fmt" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/dto/admin" + "github.com/teamhanko/hanko/backend/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestOtpAdminSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(otpAdminSuite)) +} + +type otpAdminSuite struct { + test.Suite +} + +func (s *otpAdminSuite) TestOtpAdminHandler_Get() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/otp") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userId string + expectedStatusCode int + }{ + { + name: "should return otp credential", + userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", + expectedStatusCode: http.StatusOK, + }, + { + name: "should fail if user has no otp credential", + userId: "38bf5a00-d7ea-40a5-a5de-48722c148925", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userId: "customUserId", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userId: "", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userId: "30f41697-b413-43cc-8cca-d55298683607", + expectedStatusCode: http.StatusNotFound, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s/otp", currentTest.userId), nil) + + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + if http.StatusOK == rec.Code { + var otpCredential *admin.OTPDto + s.NoError(json.Unmarshal(rec.Body.Bytes(), &otpCredential)) + s.NotNil(otpCredential) + } else { + s.Require().Equal(currentTest.expectedStatusCode, rec.Code) + } + }) + } +} + +func (s *otpAdminSuite) TestOtpAdminHandler_Delete() { + if testing.Short() { + s.T().Skip("skipping test in short mode.") + } + + err := s.LoadFixtures("../test/fixtures/otp") + s.Require().NoError(err) + + e := NewAdminRouter(&test.DefaultConfig, s.Storage, nil) + + tests := []struct { + name string + userId string + expectedStatusCode int + }{ + { + name: "should delete the otp credential", + userId: "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5", + expectedStatusCode: http.StatusNoContent, + }, + { + name: "should fail if user has no otp credential", + userId: "38bf5a00-d7ea-40a5-a5de-48722c148925", + expectedStatusCode: http.StatusNotFound, + }, + { + name: "should fail on non uuid userID", + userId: "customUserId", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on empty userID", + userId: "", + expectedStatusCode: http.StatusBadRequest, + }, + { + name: "should fail on non existing user", + userId: "30f41697-b413-43cc-8cca-d55298683607", + expectedStatusCode: http.StatusNotFound, + }, + } + + for _, currentTest := range tests { + s.Run(currentTest.name, func() { + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/users/%s/otp", currentTest.userId), nil) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + s.Require().Equal(currentTest.expectedStatusCode, rec.Code) + + if http.StatusNoContent == rec.Code { + cred, err := s.Storage.GetPasswordCredentialPersister().GetByUserID(uuid.FromStringOrNil(currentTest.userId)) + s.Require().NoError(err) + s.Require().Nil(cred) + } + }) + } +} diff --git a/backend/test/fixtures/otp/emails.yaml b/backend/test/fixtures/otp/emails.yaml new file mode 100644 index 000000000..ac30172c2 --- /dev/null +++ b/backend/test/fixtures/otp/emails.yaml @@ -0,0 +1,36 @@ +- id: 51b7c175-ceb6-45ba-aae6-0092221c1b84 + user_id: b5dd5267-b462-48be-b70d-bcd6f1bbe7a5 + address: john.doe@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + user_id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + address: john.doe+1@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: e0282f3f-b211-4f0e-b777-6fabc69287c9 + user_id: e0282f3f-b211-4f0e-b777-6fabc69287c9 + address: john.doe+2@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 8bb4c8a7-a3e6-48bb-b54f-20e3b485ab33 + user_id: b5dd5267-b462-48be-b70d-bcd6f1bbe7a5 + address: john.doe+3@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: f194ee0f-dd1a-48f7-8766-c67e4d6cd1fe + user_id: b5dd5267-b462-48be-b70d-bcd6f1bbe7a5 + address: john.doe+4@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 0394ad95-6ea3-4cbd-940b-3f6dd10440b0 + user_id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + address: john.doe+5@example.com + verified: true + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 diff --git a/backend/test/fixtures/otp/otp_secrets.yaml b/backend/test/fixtures/otp/otp_secrets.yaml new file mode 100644 index 000000000..78a455b6e --- /dev/null +++ b/backend/test/fixtures/otp/otp_secrets.yaml @@ -0,0 +1,5 @@ +- id: f28b15df-6e09-4ac0-b49f-e4e2d274f939 + user_id: b5dd5267-b462-48be-b70d-bcd6f1bbe7a5 + secret: RANDOMOTPSECRET + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 diff --git a/backend/test/fixtures/otp/primary_emails.yaml b/backend/test/fixtures/otp/primary_emails.yaml new file mode 100644 index 000000000..4c941420b --- /dev/null +++ b/backend/test/fixtures/otp/primary_emails.yaml @@ -0,0 +1,10 @@ +- id: 8fe72e5f-edb6-40e7-83a7-a7e858c2c62d + email_id: 51b7c175-ceb6-45ba-aae6-0092221c1b84 + user_id: b5dd5267-b462-48be-b70d-bcd6f1bbe7a5 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 3a5f340f-07f7-40dc-a507-d5919915e11d + email_id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + user_id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 diff --git a/backend/test/fixtures/otp/users.yaml b/backend/test/fixtures/otp/users.yaml new file mode 100644 index 000000000..05c64628f --- /dev/null +++ b/backend/test/fixtures/otp/users.yaml @@ -0,0 +1,13 @@ +- id: b5dd5267-b462-48be-b70d-bcd6f1bbe7a5 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: 38bf5a00-d7ea-40a5-a5de-48722c148925 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: e0282f3f-b211-4f0e-b777-6fabc69287c9 + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 +- id: d41df4b7-c055-45e6-9faf-61aa92a4032e + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 + From 3cdc3194469c2c37e61c3e1d41b2095954e02b33 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Fri, 29 Nov 2024 11:19:53 +0100 Subject: [PATCH 12/13] chore: merge both loadDto functions --- backend/handler/email_admin.go | 17 --------------- backend/handler/otp_admin.go | 4 ++-- backend/handler/password_admin.go | 8 +++---- backend/handler/session_admin.go | 4 ++-- backend/handler/utils.go | 23 ++++++++++++++++++++ backend/handler/webauthn_credential_admin.go | 23 +++----------------- 6 files changed, 34 insertions(+), 45 deletions(-) create mode 100644 backend/handler/utils.go diff --git a/backend/handler/email_admin.go b/backend/handler/email_admin.go index 67c09cffe..c0c2fe229 100644 --- a/backend/handler/email_admin.go +++ b/backend/handler/email_admin.go @@ -42,23 +42,6 @@ func NewEmailAdminHandler(cfg *config.Config, persister persistence.Persister) E } } -func loadDto[I admin.EmailRequests](ctx echo.Context) (*I, error) { - var adminDto I - err := ctx.Bind(&adminDto) - if err != nil { - ctx.Logger().Error(err) - return nil, echo.NewHTTPError(http.StatusBadRequest, err) - } - - err = ctx.Validate(adminDto) - if err != nil { - ctx.Logger().Error(err) - return nil, echo.NewHTTPError(http.StatusBadRequest, err) - } - - return &adminDto, nil -} - func (h *emailAdminHandler) List(ctx echo.Context) error { listDto, err := loadDto[admin.ListEmailRequestDto](ctx) if err != nil { diff --git a/backend/handler/otp_admin.go b/backend/handler/otp_admin.go index 52be2cd7c..0e1935eea 100644 --- a/backend/handler/otp_admin.go +++ b/backend/handler/otp_admin.go @@ -23,7 +23,7 @@ func NewOTPAdminHandler(persister persistence.Persister) OTPAdminHandler { } func (h *otpAdminHandler) Get(ctx echo.Context) error { - getDto, err := loadDto2[admin.GetOTPRequestDto](ctx) + getDto, err := loadDto[admin.GetOTPRequestDto](ctx) if err != nil { return err } @@ -49,7 +49,7 @@ func (h *otpAdminHandler) Get(ctx echo.Context) error { } func (h *otpAdminHandler) Delete(ctx echo.Context) error { - deleteDto, err := loadDto2[admin.GetOTPRequestDto](ctx) + deleteDto, err := loadDto[admin.GetOTPRequestDto](ctx) if err != nil { return err } diff --git a/backend/handler/password_admin.go b/backend/handler/password_admin.go index 10a1c5211..bf3e39ce2 100644 --- a/backend/handler/password_admin.go +++ b/backend/handler/password_admin.go @@ -30,7 +30,7 @@ func NewPasswordAdminHandler(persister persistence.Persister) PasswordAdminHandl } func (h *passwordAdminHandler) Get(ctx echo.Context) error { - getDto, err := loadDto2[admin.GetPasswordCredentialRequestDto](ctx) + getDto, err := loadDto[admin.GetPasswordCredentialRequestDto](ctx) if err != nil { return err } @@ -68,7 +68,7 @@ func (h *passwordAdminHandler) Get(ctx echo.Context) error { } func (h *passwordAdminHandler) Create(ctx echo.Context) error { - createDto, err := loadDto2[admin.CreateOrUpdatePasswordCredentialRequestDto](ctx) + createDto, err := loadDto[admin.CreateOrUpdatePasswordCredentialRequestDto](ctx) if err != nil { return err } @@ -116,7 +116,7 @@ func (h *passwordAdminHandler) Create(ctx echo.Context) error { } func (h *passwordAdminHandler) Update(ctx echo.Context) error { - updateDto, err := loadDto2[admin.CreateOrUpdatePasswordCredentialRequestDto](ctx) + updateDto, err := loadDto[admin.CreateOrUpdatePasswordCredentialRequestDto](ctx) if err != nil { return err } @@ -164,7 +164,7 @@ func (h *passwordAdminHandler) Update(ctx echo.Context) error { } func (h *passwordAdminHandler) Delete(ctx echo.Context) error { - getDto, err := loadDto2[admin.GetPasswordCredentialRequestDto](ctx) + getDto, err := loadDto[admin.GetPasswordCredentialRequestDto](ctx) if err != nil { return err } diff --git a/backend/handler/session_admin.go b/backend/handler/session_admin.go index bf36deb4b..c9c7ec55f 100644 --- a/backend/handler/session_admin.go +++ b/backend/handler/session_admin.go @@ -114,7 +114,7 @@ func (h *SessionAdminHandler) Generate(ctx echo.Context) error { } func (h *SessionAdminHandler) List(ctx echo.Context) error { - listDto, err := loadDto2[admin.ListSessionsRequestDto](ctx) + listDto, err := loadDto[admin.ListSessionsRequestDto](ctx) if err != nil { return err } @@ -142,7 +142,7 @@ func (h *SessionAdminHandler) List(ctx echo.Context) error { } func (h *SessionAdminHandler) Delete(ctx echo.Context) error { - deleteDto, err := loadDto2[admin.DeleteSessionRequestDto](ctx) + deleteDto, err := loadDto[admin.DeleteSessionRequestDto](ctx) if err != nil { return err } diff --git a/backend/handler/utils.go b/backend/handler/utils.go new file mode 100644 index 000000000..3c2cbeef8 --- /dev/null +++ b/backend/handler/utils.go @@ -0,0 +1,23 @@ +package handler + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +func loadDto[I any](ctx echo.Context) (*I, error) { + var adminDto I + err := ctx.Bind(&adminDto) + if err != nil { + ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusBadRequest, err) + } + + err = ctx.Validate(adminDto) + if err != nil { + ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusBadRequest, err) + } + + return &adminDto, nil +} diff --git a/backend/handler/webauthn_credential_admin.go b/backend/handler/webauthn_credential_admin.go index 086406c29..8470e8d09 100644 --- a/backend/handler/webauthn_credential_admin.go +++ b/backend/handler/webauthn_credential_admin.go @@ -27,7 +27,7 @@ func NewWebauthnCredentialAdminHandler(persister persistence.Persister) Webauthn } func (h *webauthnCredentialAdminHandler) List(ctx echo.Context) error { - listDto, err := loadDto2[admin.ListWebauthnCredentialsRequestDto](ctx) + listDto, err := loadDto[admin.ListWebauthnCredentialsRequestDto](ctx) if err != nil { return err } @@ -60,7 +60,7 @@ func (h *webauthnCredentialAdminHandler) List(ctx echo.Context) error { } func (h *webauthnCredentialAdminHandler) Get(ctx echo.Context) error { - getDto, err := loadDto2[admin.GetWebauthnCredentialRequestDto](ctx) + getDto, err := loadDto[admin.GetWebauthnCredentialRequestDto](ctx) if err != nil { return err } @@ -92,7 +92,7 @@ func (h *webauthnCredentialAdminHandler) Get(ctx echo.Context) error { } func (h *webauthnCredentialAdminHandler) Delete(ctx echo.Context) error { - deleteDto, err := loadDto2[admin.GetWebauthnCredentialRequestDto](ctx) + deleteDto, err := loadDto[admin.GetWebauthnCredentialRequestDto](ctx) if err != nil { return err } @@ -127,20 +127,3 @@ func (h *webauthnCredentialAdminHandler) Delete(ctx echo.Context) error { return ctx.NoContent(http.StatusNoContent) } - -func loadDto2[I any](ctx echo.Context) (*I, error) { - var adminDto I - err := ctx.Bind(&adminDto) - if err != nil { - ctx.Logger().Error(err) - return nil, echo.NewHTTPError(http.StatusBadRequest, err) - } - - err = ctx.Validate(adminDto) - if err != nil { - ctx.Logger().Error(err) - return nil, echo.NewHTTPError(http.StatusBadRequest, err) - } - - return &adminDto, nil -} From fb4db99f78567b04bff18e2027417c2498d7dc35 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Fri, 29 Nov 2024 11:27:59 +0100 Subject: [PATCH 13/13] tests: fix test name typos --- backend/handler/session_admin_test.go | 6 +++--- backend/handler/webauthn_credential_admin_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/handler/session_admin_test.go b/backend/handler/session_admin_test.go index 9ed6f833a..975144c5d 100644 --- a/backend/handler/session_admin_test.go +++ b/backend/handler/session_admin_test.go @@ -44,7 +44,7 @@ func (s *sessionAdminSuite) TestSessionAdminHandler_List() { expectedCount: 2, }, { - name: "should return a list of sessions with one entries", + name: "should return a list of sessions with one entry", userID: "38bf5a00-d7ea-40a5-a5de-48722c148925", expectedStatusCode: http.StatusOK, expectedCount: 1, @@ -132,7 +132,7 @@ func (s *sessionAdminSuite) TestSessionAdminHandler_Delete() { expectedStatusCode: http.StatusNotFound, }, { - name: "should fail for if credential is not associated to the user", + name: "should fail if session is not associated to the user", userID: "38bf5a00-d7ea-40a5-a5de-48722c148925", sessionID: "74ba812a-923a-43e4-8020-9535dcadc0a8", expectedStatusCode: http.StatusNotFound, @@ -150,7 +150,7 @@ func (s *sessionAdminSuite) TestSessionAdminHandler_Delete() { expectedStatusCode: http.StatusBadRequest, }, { - name: "should fail on empty credentialID", + name: "should fail on empty sessionID", userID: "46626836-f2db-4ec0-8752-858b544cbc78", sessionID: "", expectedStatusCode: http.StatusNotFound, diff --git a/backend/handler/webauthn_credential_admin_test.go b/backend/handler/webauthn_credential_admin_test.go index 829296861..8def308ce 100644 --- a/backend/handler/webauthn_credential_admin_test.go +++ b/backend/handler/webauthn_credential_admin_test.go @@ -120,7 +120,7 @@ func (s *webauthnCredentialAdminSuite) TestWebauthnCredentialAdminHandler_Get() expectedStatusCode: http.StatusNotFound, }, { - name: "should fail for if credential is not associated to the user", + name: "should fail if credential is not associated to the user", userID: "ec4ef049-5b88-4321-a173-21b0eff06a04", credentialID: "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM", expectedStatusCode: http.StatusNotFound, @@ -207,7 +207,7 @@ func (s *webauthnCredentialAdminSuite) TestWebauthnCredentialAdminHandler_Delete expectedStatusCode: http.StatusNotFound, }, { - name: "should fail for if credential is not associated to the user", + name: "should fail if credential is not associated to the user", userID: "46626836-f2db-4ec0-8752-858b544cbc78", credentialID: "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjK", expectedStatusCode: http.StatusNotFound,