Skip to content

Commit

Permalink
Admin api changes (#1974)
Browse files Browse the repository at this point in the history
* feat: return mfa only flag

* feat: add webauthn admin handler

* feat: add webauthn credential handler to router

* feat: add password mgmt admin endpoints

* feat: add sessions admin handler

* feat: add otp admin handler

* feat: add otp to admin user dto

* test: add admin password handler test

* test: add admin webauthn handler test

* test: add admin session handler test

* test: add admin otp handler test

* chore: merge both loadDto functions

* tests: fix test name typos
  • Loading branch information
FreddyDevelop authored Dec 3, 2024
1 parent aa97808 commit c264108
Show file tree
Hide file tree
Showing 28 changed files with 1,617 additions and 31 deletions.
15 changes: 15 additions & 0 deletions backend/dto/admin/otp.go
Original file line number Diff line number Diff line change
@@ -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"`
}
9 changes: 9 additions & 0 deletions backend/dto/admin/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
9 changes: 9 additions & 0 deletions backend/dto/admin/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
10 changes: 10 additions & 0 deletions backend/dto/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -55,6 +64,7 @@ func FromUserModel(model models.User) User {
UpdatedAt: model.UpdatedAt,
Password: passwordCredential,
Identities: identities,
OTP: otp,
}
}

Expand Down
10 changes: 10 additions & 0 deletions backend/dto/admin/webauthn.go
Original file line number Diff line number Diff line change
@@ -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"`
}
2 changes: 2 additions & 0 deletions backend/dto/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,5 +37,6 @@ func FromWebauthnCredentialModel(c *models.WebauthnCredential) *WebauthnCredenti
Transports: c.Transports.GetNames(),
BackupEligible: c.BackupEligible,
BackupState: c.BackupState,
MFAOnly: c.MFAOnly,
}
}
5 changes: 1 addition & 4 deletions backend/flow_api/services/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
}
}

Expand Down
24 changes: 23 additions & 1 deletion backend/handler/admin_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -74,6 +75,28 @@ 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)

passwordCredentialHandler := NewPasswordAdminHandler(persister)
passwordCredentials := user.Group("/:user_id/password")
passwordCredentials.GET("", passwordCredentialHandler.Get)
passwordCredentials.POST("", passwordCredentialHandler.Create)
passwordCredentials.PUT("", passwordCredentialHandler.Update)
passwordCredentials.DELETE("", passwordCredentialHandler.Delete)

userSessions := user.Group("/:user_id/sessions")
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")
Expand All @@ -87,7 +110,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)

Expand Down
17 changes: 0 additions & 17 deletions backend/handler/email_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
77 changes: 77 additions & 0 deletions backend/handler/otp_admin.go
Original file line number Diff line number Diff line change
@@ -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 := loadDto[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 := loadDto[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)
}
Loading

0 comments on commit c264108

Please sign in to comment.