diff --git a/.gitignore b/.gitignore index 3570459..e45fd52 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ *.so *.dylib + + #env files *.env @@ -24,4 +26,4 @@ *.iml # version -/backend/build_info/version.txt +server/build_info/version.txt diff --git a/deploy/docker-compose/backend.yaml b/deploy/docker-compose/backend.yaml index d9de26b..b1b4908 100644 --- a/deploy/docker-compose/backend.yaml +++ b/deploy/docker-compose/backend.yaml @@ -19,9 +19,10 @@ services: - type: bind source: ./config.yaml target: /etc/config/config.yaml - command: --config /etc/config/config.yaml serve + command: --config /etc/config/config.yaml serve all ports: - '8000:8000' + - '8001:8001' restart: unless-stopped depends_on: passkey-migrate: @@ -37,7 +38,7 @@ services: - POSTGRES_PASSWORD=hanko - POSTGRES_DB=passkey healthcheck: - test: pg_isready -U hanko -d hanko + test: pg_isready -U hanko -d passkey interval: 10s timeout: 10s retries: 3 diff --git a/deploy/docker-compose/config.yaml b/deploy/docker-compose/config.yaml index 3e63d06..c170e92 100644 --- a/deploy/docker-compose/config.yaml +++ b/deploy/docker-compose/config.yaml @@ -5,8 +5,3 @@ database: port: 5432 user: hanko password: hanko -secrets: - api_keys: - - 2d92ff02-a646-4ddb-948a-931f122b4371 - keys: - - super-long-and-super-secret diff --git a/server/api/api.go b/server/api/api.go index 7157b6d..19e0d3f 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -1,15 +1,23 @@ package api import ( - router2 "github.com/teamhanko/passkey-server/api/router" + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/api/router" "github.com/teamhanko/passkey-server/config" "github.com/teamhanko/passkey-server/persistence" "sync" ) -func Start(cfg *config.Config, wg *sync.WaitGroup, persister persistence.Persister) { +func StartPublic(cfg *config.Config, wg *sync.WaitGroup, persister persistence.Persister) { defer wg.Done() - router := router2.NewMainRouter(cfg, persister) - router.Logger.Fatal(router.Start(cfg.Server.Address)) + mainRouter := router.NewMainRouter(cfg, persister) + mainRouter.Logger.Fatal(mainRouter.Start(cfg.Address)) +} + +func StartAdmin(cfg *config.Config, wg *sync.WaitGroup, persister persistence.Persister, prometheus echo.MiddlewareFunc) { + defer wg.Done() + + adminRouter := router.NewAdminRouter(cfg, persister, prometheus) + adminRouter.Logger.Fatal(adminRouter.Start(cfg.AdminAddress)) } diff --git a/server/api/dto/admin/request/config.go b/server/api/dto/admin/request/config.go new file mode 100644 index 0000000..d087d67 --- /dev/null +++ b/server/api/dto/admin/request/config.go @@ -0,0 +1,45 @@ +package request + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type CreateConfigDto struct { + Cors CreateCorsDto `json:"cors" validate:"required"` + Webauthn CreateWebauthnDto `json:"webauthn" validate:"required"` +} + +func (dto *CreateConfigDto) ToModel(tenant models.Tenant) models.Config { + configId, _ := uuid.NewV4() + now := time.Now() + + auditLogId, _ := uuid.NewV4() + auditLogModel := models.AuditLogConfig{ + ID: auditLogId, + ConfigID: configId, + OutputStream: config.OutputStreamStdOut, + ConsoleEnabled: true, + StorageEnabled: true, + CreatedAt: now, + UpdatedAt: now, + } + + configModel := models.Config{ + ID: configId, + TenantID: tenant.ID, + AuditLogConfig: auditLogModel, + Secrets: nil, + CreatedAt: now, + UpdatedAt: now, + } + + return configModel +} + +type UpdateConfigDto struct { + GetTenantDto + CreateConfigDto +} diff --git a/server/api/dto/admin/request/cors.go b/server/api/dto/admin/request/cors.go new file mode 100644 index 0000000..962cbe5 --- /dev/null +++ b/server/api/dto/admin/request/cors.go @@ -0,0 +1,42 @@ +package request + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type CreateCorsDto struct { + AllowedOrigins []string `json:"allowed_origins" validate:"required,min=1"` + AllowUnsafeWildcard bool `json:"allow_unsafe_wildcard" validate:"required,boolean"` +} + +func (dto *CreateCorsDto) ToModel(config models.Config) models.Cors { + corsId, _ := uuid.NewV4() + now := time.Now() + + var origins models.CorsOrigins + + for _, origin := range dto.AllowedOrigins { + originId, _ := uuid.NewV4() + originModel := models.CorsOrigin{ + ID: originId, + Origin: origin, + CreatedAt: now, + UpdatedAt: now, + } + + origins = append(origins, originModel) + } + + cors := models.Cors{ + ID: corsId, + ConfigID: config.ID, + AllowUnsafe: dto.AllowUnsafeWildcard, + Origins: origins, + CreatedAt: now, + UpdatedAt: now, + } + + return cors +} diff --git a/server/api/dto/admin/request/relying_party.go b/server/api/dto/admin/request/relying_party.go new file mode 100644 index 0000000..ccf98a5 --- /dev/null +++ b/server/api/dto/admin/request/relying_party.go @@ -0,0 +1,45 @@ +package request + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type CreateRelyingPartyDto struct { + Id string `json:"id" validate:"required"` + DisplayName string `json:"display_name" validate:"required"` + Icon *string `json:"icon"` + Origins []string `json:"origins"` +} + +func (dto *CreateRelyingPartyDto) ToModel(config models.WebauthnConfig) models.RelyingParty { + rpId, _ := uuid.NewV4() + now := time.Now() + var origins models.WebauthnOrigins + + for _, origin := range dto.Origins { + originId, _ := uuid.NewV4() + originModel := models.WebauthnOrigin{ + ID: originId, + Origin: origin, + CreatedAt: now, + UpdatedAt: now, + } + + origins = append(origins, originModel) + } + + relyingParty := models.RelyingParty{ + ID: rpId, + WebauthnConfigID: config.ID, + RPId: dto.Id, + DisplayName: dto.DisplayName, + Icon: dto.Icon, + Origins: origins, + CreatedAt: now, + UpdatedAt: now, + } + + return relyingParty +} diff --git a/server/api/dto/admin/request/secrets.go b/server/api/dto/admin/request/secrets.go new file mode 100644 index 0000000..dc19084 --- /dev/null +++ b/server/api/dto/admin/request/secrets.go @@ -0,0 +1,39 @@ +package request + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/crypto" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type CreateSecretDto struct { + GetTenantDto + Name string `json:"name"` +} + +func (dto *CreateSecretDto) ToModel(config *models.Config, isApiKey bool) (*models.Secret, error) { + secretId, _ := uuid.NewV4() + + secretKey, err := crypto.GenerateRandomStringURLSafe(64) + if err != nil { + return nil, err + } + + now := time.Now() + + return &models.Secret{ + ID: secretId, + Name: dto.Name, + Key: secretKey, + IsAPISecret: isApiKey, + Config: config, + CreatedAt: now, + UpdatedAt: now, + }, nil +} + +type RemoveSecretDto struct { + GetTenantDto + SecretId string `param:"secret_id" validate:"required,uuid4"` +} diff --git a/server/api/dto/admin/request/tenant.go b/server/api/dto/admin/request/tenant.go new file mode 100644 index 0000000..63a9fd4 --- /dev/null +++ b/server/api/dto/admin/request/tenant.go @@ -0,0 +1,35 @@ +package request + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type CreateTenantDto struct { + DisplayName string `json:"display_name" validate:"required"` + Config CreateConfigDto `json:"config" validate:"required"` +} + +func (dto *CreateTenantDto) ToModel() models.Tenant { + tenantId, _ := uuid.NewV4() + now := time.Now() + + tenant := models.Tenant{ + ID: tenantId, + DisplayName: dto.DisplayName, + CreatedAt: now, + UpdatedAt: now, + } + + return tenant +} + +type GetTenantDto struct { + Id string `param:"tenant_id" validate:"required,uuid4"` +} + +type UpdateTenantDto struct { + GetTenantDto + DisplayName string `json:"display_name" validate:"required"` +} diff --git a/server/api/dto/admin/request/webauthn.go b/server/api/dto/admin/request/webauthn.go new file mode 100644 index 0000000..cbe22f3 --- /dev/null +++ b/server/api/dto/admin/request/webauthn.go @@ -0,0 +1,30 @@ +package request + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type CreateWebauthnDto struct { + RelyingParty CreateRelyingPartyDto `json:"relying_party" validate:"required"` + Timeout int `json:"timeout" validate:"required,number"` + UserVerification protocol.UserVerificationRequirement `json:"user_verification" validate:"required,oneof=required preferred discouraged"` +} + +func (dto *CreateWebauthnDto) ToModel(configModel models.Config) models.WebauthnConfig { + webauthnConfigId, _ := uuid.NewV4() + now := time.Now() + + webauthnConfig := models.WebauthnConfig{ + ID: webauthnConfigId, + ConfigID: configModel.ID, + Timeout: dto.Timeout, + CreatedAt: now, + UpdatedAt: now, + UserVerification: dto.UserVerification, + } + + return webauthnConfig +} diff --git a/server/api/dto/admin/response/config.go b/server/api/dto/admin/response/config.go new file mode 100644 index 0000000..05c9abb --- /dev/null +++ b/server/api/dto/admin/response/config.go @@ -0,0 +1,15 @@ +package response + +import "github.com/teamhanko/passkey-server/persistence/models" + +type GetConfigResponse struct { + Cors GetCorsResponse `json:"cors"` + Webauthn GetWebauthnResponse `json:"webauthn"` +} + +func ToGetConfigResponse(config *models.Config) GetConfigResponse { + return GetConfigResponse{ + Cors: ToGetCorsResponse(&config.Cors), + Webauthn: ToGetWebauthnResponse(&config.WebauthnConfig), + } +} diff --git a/server/api/dto/admin/response/cors.go b/server/api/dto/admin/response/cors.go new file mode 100644 index 0000000..76d27ff --- /dev/null +++ b/server/api/dto/admin/response/cors.go @@ -0,0 +1,20 @@ +package response + +import "github.com/teamhanko/passkey-server/persistence/models" + +type GetCorsResponse struct { + AllowedOrigins []string `json:"allowed_origins"` + AllowUnsafe bool `json:"allow_unsafe_wildcard"` +} + +func ToGetCorsResponse(cors *models.Cors) GetCorsResponse { + var origins []string + for _, origin := range cors.Origins { + origins = append(origins, origin.Origin) + } + + return GetCorsResponse{ + AllowedOrigins: origins, + AllowUnsafe: cors.AllowUnsafe, + } +} diff --git a/server/api/dto/admin/response/relying_party.go b/server/api/dto/admin/response/relying_party.go new file mode 100644 index 0000000..43a4ab2 --- /dev/null +++ b/server/api/dto/admin/response/relying_party.go @@ -0,0 +1,24 @@ +package response + +import "github.com/teamhanko/passkey-server/persistence/models" + +type GetRelyingPartyResponse struct { + Id string `json:"id"` + DisplayName string `json:"display_name"` + Icon *string `json:"icon,omitempty"` + Origins []string `json:"origins"` +} + +func ToGetRelyingPartyResponse(relyingParty *models.RelyingParty) GetRelyingPartyResponse { + var origins []string + for _, origin := range relyingParty.Origins { + origins = append(origins, origin.Origin) + } + + return GetRelyingPartyResponse{ + Id: relyingParty.RPId, + DisplayName: relyingParty.DisplayName, + Icon: relyingParty.Icon, + Origins: origins, + } +} diff --git a/server/api/dto/admin/response/secrets.go b/server/api/dto/admin/response/secrets.go new file mode 100644 index 0000000..884f61f --- /dev/null +++ b/server/api/dto/admin/response/secrets.go @@ -0,0 +1,23 @@ +package response + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" + "time" +) + +type SecretResponseDto struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` + Secret string `json:"secret"` + CreatedAt time.Time `json:"created_at"` +} + +func ToSecretResponse(secret *models.Secret) SecretResponseDto { + return SecretResponseDto{ + Id: secret.ID, + Name: secret.Name, + Secret: secret.Key, + CreatedAt: secret.CreatedAt, + } +} diff --git a/server/api/dto/admin/response/tenant.go b/server/api/dto/admin/response/tenant.go new file mode 100644 index 0000000..5cd6e3e --- /dev/null +++ b/server/api/dto/admin/response/tenant.go @@ -0,0 +1,42 @@ +package response + +import ( + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type ListTenantResponse struct { + Id uuid.UUID `json:"id"` + DisplayName string `json:"display_name"` +} + +func ToListTenantResponse(tenant *models.Tenant) ListTenantResponse { + return ListTenantResponse{ + Id: tenant.ID, + DisplayName: tenant.DisplayName, + } +} + +type GetTenantResponse struct { + ListTenantResponse + Config GetConfigResponse `json:"config"` +} + +func ToGetTenantResponse(tenant *models.Tenant) GetTenantResponse { + return GetTenantResponse{ + ListTenantResponse: ToListTenantResponse(tenant), + Config: ToGetConfigResponse(&tenant.Config), + } +} + +type CreateTenantResponse struct { + Id uuid.UUID `json:"id"` + ApiKey SecretResponseDto `json:"api_key"` +} + +func ToCreateTenantResponse(tenant *models.Tenant, apiKey *models.Secret) CreateTenantResponse { + return CreateTenantResponse{ + Id: tenant.ID, + ApiKey: ToSecretResponse(apiKey), + } +} diff --git a/server/api/dto/admin/response/webatuhn.go b/server/api/dto/admin/response/webatuhn.go new file mode 100644 index 0000000..2fdb290 --- /dev/null +++ b/server/api/dto/admin/response/webatuhn.go @@ -0,0 +1,20 @@ +package response + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type GetWebauthnResponse struct { + RelyingParty GetRelyingPartyResponse `json:"relying_party"` + Timeout int `json:"timeout"` + UserVerification protocol.UserVerificationRequirement `json:"user_verification"` +} + +func ToGetWebauthnResponse(webauthn *models.WebauthnConfig) GetWebauthnResponse { + return GetWebauthnResponse{ + RelyingParty: ToGetRelyingPartyResponse(&webauthn.RelyingParty), + Timeout: webauthn.Timeout, + UserVerification: webauthn.UserVerification, + } +} diff --git a/server/api/dto/intern/webauthn_credential.go b/server/api/dto/intern/webauthn_credential.go index f4bb36c..11fa60d 100644 --- a/server/api/dto/intern/webauthn_credential.go +++ b/server/api/dto/intern/webauthn_credential.go @@ -2,6 +2,7 @@ package intern import ( "encoding/base64" + "fmt" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/gofrs/uuid" @@ -9,13 +10,15 @@ import ( "time" ) -func WebauthnCredentialToModel(credential *webauthn.Credential, userId uuid.UUID, backupEligible bool, backupState bool) *models.WebauthnCredential { +func WebauthnCredentialToModel(credential *webauthn.Credential, userId uuid.UUID, webauthnUserId uuid.UUID, backupEligible bool, backupState bool) *models.WebauthnCredential { now := time.Now().UTC() aaguid, _ := uuid.FromBytes(credential.Authenticator.AAGUID) credentialID := base64.RawURLEncoding.EncodeToString(credential.ID) + name := fmt.Sprintf("cred-%s", credentialID) c := &models.WebauthnCredential{ ID: credentialID, + Name: &name, UserId: userId, PublicKey: base64.RawURLEncoding.EncodeToString(credential.PublicKey), AttestationType: credential.AttestationType, @@ -26,6 +29,8 @@ func WebauthnCredentialToModel(credential *webauthn.Credential, userId uuid.UUID UpdatedAt: now, BackupEligible: backupEligible, BackupState: backupState, + + WebauthnUserID: webauthnUserId, } for _, name := range credential.Transport { diff --git a/server/api/dto/intern/webauthn_session_data.go b/server/api/dto/intern/webauthn_session_data.go index a410f58..a424ecc 100644 --- a/server/api/dto/intern/webauthn_session_data.go +++ b/server/api/dto/intern/webauthn_session_data.go @@ -32,7 +32,7 @@ func WebauthnSessionDataFromModel(data *models.WebauthnSessionData) *webauthn.Se } } -func WebauthnSessionDataToModel(data *webauthn.SessionData, operation models.Operation) *models.WebauthnSessionData { +func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID, operation models.Operation) *models.WebauthnSessionData { id, _ := uuid.NewV4() userId, _ := uuid.FromBytes(data.UserID) now := time.Now() @@ -61,5 +61,6 @@ func WebauthnSessionDataToModel(data *webauthn.SessionData, operation models.Ope Operation: operation, AllowedCredentials: allowedCredentials, ExpiresAt: nulls.NewTime(data.Expires), + TenantID: tenantId, } } diff --git a/server/api/dto/intern/webauthn_user.go b/server/api/dto/intern/webauthn_user.go index 7720f87..e03a0ae 100644 --- a/server/api/dto/intern/webauthn_user.go +++ b/server/api/dto/intern/webauthn_user.go @@ -20,7 +20,7 @@ func NewWebauthnUser(user models.WebauthnUser) *WebauthnUser { Name: user.Name, Icon: user.Icon, DisplayName: user.DisplayName, - WebauthnCredentials: user.Credentials, + WebauthnCredentials: user.WebauthnCredentials, } } diff --git a/server/api/dto/request/requests.go b/server/api/dto/request/requests.go index bda1b89..d1524c9 100644 --- a/server/api/dto/request/requests.go +++ b/server/api/dto/request/requests.go @@ -4,6 +4,10 @@ type CredentialRequest interface { ListCredentialsDto | DeleteCredentialsDto | UpdateCredentialsDto } +type TenantDto struct { + TenantId string `param:"tenant_id" validate:"required,uuid4"` +} + type ListCredentialsDto struct { UserId string `query:"user_id" validate:"required,uuid4"` } diff --git a/server/api/handler/admin/health.go b/server/api/handler/admin/health.go new file mode 100644 index 0000000..0aef2f2 --- /dev/null +++ b/server/api/handler/admin/health.go @@ -0,0 +1,20 @@ +package admin + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +type HealthHandler struct{} + +func NewHealthHandler() *HealthHandler { + return &HealthHandler{} +} + +func (handler *HealthHandler) Ready(ctx echo.Context) error { + return ctx.JSON(http.StatusOK, map[string]bool{"ready": true}) +} + +func (handler *HealthHandler) Alive(ctx echo.Context) error { + return ctx.JSON(http.StatusOK, map[string]bool{"alive": true}) +} diff --git a/server/api/handler/admin/secrets.go b/server/api/handler/admin/secrets.go new file mode 100644 index 0000000..f5c9b0a --- /dev/null +++ b/server/api/handler/admin/secrets.go @@ -0,0 +1,144 @@ +package admin + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + adminRequest "github.com/teamhanko/passkey-server/api/dto/admin/request" + "github.com/teamhanko/passkey-server/api/dto/admin/response" + "github.com/teamhanko/passkey-server/api/helper" + "github.com/teamhanko/passkey-server/persistence" + "github.com/teamhanko/passkey-server/persistence/models" + "net/http" +) + +type SecretsHandler struct { + persister persistence.Persister +} + +func (s *SecretsHandler) ListAPIKeys(ctx echo.Context) error { + return s.listKeys(ctx, true) +} + +func (s *SecretsHandler) listKeys(ctx echo.Context, isApiKey bool) error { + var dto adminRequest.GetTenantDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to list keys").SetInternal(err) + } + + tenant, err := s.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + secrets := make([]response.SecretResponseDto, 0) + for _, secret := range tenant.Config.Secrets { + if secret.IsAPISecret == isApiKey { + secrets = append(secrets, response.ToSecretResponse(&secret)) + } + } + + return ctx.JSON(http.StatusOK, secrets) +} + +func (s *SecretsHandler) findTenantByIdString(id string) (*models.Tenant, error) { + return helper.FindTenantByIdString(id, s.persister.GetTenantPersister(nil)) +} + +func (s *SecretsHandler) CreateAPIKey(ctx echo.Context) error { + return s.createKey(ctx, true) +} + +func (s *SecretsHandler) createKey(ctx echo.Context, isApiKey bool) error { + var dto adminRequest.CreateSecretDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to create key").SetInternal(err) + } + + tenant, err := s.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + secret, err := dto.ToModel(&tenant.Config, isApiKey) + if err != nil { + ctx.Logger().Error(err) + return err + } + + err = s.persister.GetSecretsPersister(nil).Create(secret) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return ctx.JSON(http.StatusCreated, response.ToSecretResponse(secret)) +} + +func (s *SecretsHandler) RemoveAPIKey(ctx echo.Context) error { + return s.removeKey(ctx, true) +} + +func (s *SecretsHandler) removeKey(ctx echo.Context, isApiKey bool) error { + var dto adminRequest.RemoveSecretDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to remove key").SetInternal(err) + } + + tenant, err := s.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + secretId, err := uuid.FromString(dto.SecretId) + if err != nil { + ctx.Logger().Error(err) + return err + } + + var foundSecret *models.Secret + for _, secret := range tenant.Config.Secrets { + if secret.ID == secretId && secret.IsAPISecret == isApiKey { + foundSecret = &secret + } + } + + if foundSecret == nil { + return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("secret with ID '%s' not found", dto.SecretId)) + } + + err = s.persister.GetSecretsPersister(nil).Delete(foundSecret) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return ctx.NoContent(http.StatusNoContent) +} + +func (s *SecretsHandler) ListJWKKeys(ctx echo.Context) error { + return s.listKeys(ctx, false) +} + +func (s *SecretsHandler) CreateJWKKey(ctx echo.Context) error { + return s.createKey(ctx, false) +} + +func (s *SecretsHandler) RemoveJWKKey(ctx echo.Context) error { + return s.removeKey(ctx, false) +} + +func NewSecretsHandler(persister persistence.Persister) SecretsHandler { + return SecretsHandler{ + persister: persister, + } +} diff --git a/server/api/handler/admin/status.go b/server/api/handler/admin/status.go new file mode 100644 index 0000000..c627770 --- /dev/null +++ b/server/api/handler/admin/status.go @@ -0,0 +1,29 @@ +package admin + +import ( + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/persistence" + "net/http" +) + +type StatusHandler struct { + persister persistence.Persister +} + +func NewStatusHandler(persister persistence.Persister) *StatusHandler { + return &StatusHandler{ + persister: persister, + } +} + +func (h *StatusHandler) Status(ctx echo.Context) error { + // random query to check DB connectivity + _, err := h.persister.GetJwkPersister(nil).GetAll() + status := http.StatusOK + if err != nil { + ctx.Logger().Error(err) + status = http.StatusInternalServerError + } + + return ctx.Render(status, "status", map[string]bool{"dbError": err != nil}) +} diff --git a/server/api/handler/admin/tenants.go b/server/api/handler/admin/tenants.go new file mode 100644 index 0000000..710e9dd --- /dev/null +++ b/server/api/handler/admin/tenants.go @@ -0,0 +1,296 @@ +package admin + +import ( + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/api/dto/admin/request" + "github.com/teamhanko/passkey-server/api/dto/admin/response" + "github.com/teamhanko/passkey-server/api/helper" + "github.com/teamhanko/passkey-server/crypto" + hankoJwk "github.com/teamhanko/passkey-server/crypto/jwk" + "github.com/teamhanko/passkey-server/persistence" + "github.com/teamhanko/passkey-server/persistence/models" + "net/http" + "time" +) + +type TenantHandler struct { + persister persistence.Persister +} + +func (th *TenantHandler) List(ctx echo.Context) error { + tenants, err := th.persister.GetTenantPersister(nil).List() + if err != nil { + ctx.Logger().Error(err) + return err + } + + tenantList := make([]response.ListTenantResponse, 0) + for _, tenant := range tenants { + tenantList = append(tenantList, response.ToListTenantResponse(&tenant)) + } + + return ctx.JSON(http.StatusOK, tenantList) +} + +func (th *TenantHandler) Create(ctx echo.Context) error { + var dto request.CreateTenantDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to create tenant").SetInternal(err) + } + + // transform dto to model + tenantModel := dto.ToModel() + configModel := dto.Config.ToModel(tenantModel) + corsModel := dto.Config.Cors.ToModel(configModel) + webauthnConfigModel := dto.Config.Webauthn.ToModel(configModel) + relyingPartyModel := dto.Config.Webauthn.RelyingParty.ToModel(webauthnConfigModel) + + // create API secret + secretId, err := uuid.NewV4() + if err != nil { + ctx.Logger().Error(err) + return err + } + + secretKey, err := crypto.GenerateRandomStringURLSafe(64) + if err != nil { + ctx.Logger().Error(err) + return err + } + + now := time.Now() + + apiSecretModel := models.Secret{ + ID: secretId, + Name: "Initial API Key", + Key: secretKey, + ConfigID: configModel.ID, + IsAPISecret: true, + CreatedAt: now, + UpdatedAt: now, + } + + secretId, err = uuid.NewV4() + if err != nil { + ctx.Logger().Error(err) + return err + } + + secretKey, err = crypto.GenerateRandomStringURLSafe(64) + if err != nil { + ctx.Logger().Error(err) + return err + } + + jwkSecretModel := models.Secret{ + ID: secretId, + Name: "Initial JWK Key", + ConfigID: configModel.ID, + Key: secretKey, + IsAPISecret: false, + CreatedAt: now, + UpdatedAt: now, + } + + return th.persister.GetConnection().Transaction(func(tx *pop.Connection) error { + tenantPersister := th.persister.GetTenantPersister(tx) + secretPersister := th.persister.GetSecretsPersister(tx) + + err = tenantPersister.Create(&tenantModel) + if err != nil { + ctx.Logger().Error(err) + return err + } + + err = th.persistConfig(tx, &configModel, &corsModel, &webauthnConfigModel, &relyingPartyModel) + if err != nil { + ctx.Logger().Error(err) + return err + } + + err = secretPersister.Create(&apiSecretModel) + if err != nil { + ctx.Logger().Error(err) + return err + } + + err = secretPersister.Create(&jwkSecretModel) + if err != nil { + ctx.Logger().Error(err) + return err + } + + jwks := []string{jwkSecretModel.Key} + _, err := hankoJwk.NewDefaultManager(jwks, tenantModel.ID, th.persister.GetJwkPersister(tx)) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, "unable to initialize jwt generator").SetInternal(err) + } + + return ctx.JSON(http.StatusCreated, response.ToCreateTenantResponse(&tenantModel, &apiSecretModel)) + }) +} + +func (th *TenantHandler) Get(ctx echo.Context) error { + var dto request.GetTenantDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to get tenant").SetInternal(err) + } + + tenant, err := th.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return ctx.JSON(http.StatusOK, response.ToGetTenantResponse(tenant)) +} + +func (th *TenantHandler) findTenantByIdString(id string) (*models.Tenant, error) { + return helper.FindTenantByIdString(id, th.persister.GetTenantPersister(nil)) +} + +func (th *TenantHandler) Update(ctx echo.Context) error { + var dto request.UpdateTenantDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to update tenant").SetInternal(err) + } + + tenant, err := th.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + tenant.DisplayName = dto.DisplayName + tenant.UpdatedAt = time.Now() + + err = th.persister.GetTenantPersister(nil).Update(tenant) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return ctx.NoContent(http.StatusNoContent) +} + +func (th *TenantHandler) Remove(ctx echo.Context) error { + var dto request.GetTenantDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to remove tenant").SetInternal(err) + } + + tenant, err := th.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + err = th.persister.GetTenantPersister(nil).Delete(tenant) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return ctx.NoContent(http.StatusNoContent) +} + +func (th *TenantHandler) UpdateConfig(ctx echo.Context) error { + var dto request.UpdateConfigDto + err := ctx.Bind(&dto) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to update tenant config").SetInternal(err) + } + + tenant, err := th.findTenantByIdString(dto.Id) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return th.persister.GetConnection().Transaction(func(tx *pop.Connection) error { + configPersister := th.persister.GetConfigPersister(tx) + secretPersister := th.persister.GetSecretsPersister(tx) + + config := tenant.Config + newConfig := dto.ToModel(*tenant) + corsModel := dto.Cors.ToModel(newConfig) + webauthnConfigModel := dto.Webauthn.ToModel(newConfig) + relyingPartyModel := dto.Webauthn.RelyingParty.ToModel(webauthnConfigModel) + + err = th.persistConfig(tx, &newConfig, &corsModel, &webauthnConfigModel, &relyingPartyModel) + if err != nil { + ctx.Logger().Error(err) + return err + } + + for _, secret := range config.Secrets { + secret.ConfigID = newConfig.ID + err = secretPersister.Update(&secret) + if err != nil { + ctx.Logger().Error(err) + return err + } + } + + err = configPersister.Delete(&config) + if err != nil { + ctx.Logger().Error(err) + return err + } + + return ctx.NoContent(http.StatusNoContent) + }) +} + +func (th *TenantHandler) persistConfig(tx *pop.Connection, config *models.Config, cors *models.Cors, webauthn *models.WebauthnConfig, rp *models.RelyingParty) error { + configPersister := th.persister.GetConfigPersister(tx) + corsPersister := th.persister.GetCorsPersister(tx) + webauthnConfigPersister := th.persister.GetWebauthnConfigPersister(tx) + relyingPartyPersister := th.persister.GetWebauthnRelyingPartyPersister(tx) + auditLogConfigPersister := th.persister.GetAuditLogConfigPersister(tx) + + err := configPersister.Create(config) + if err != nil { + return err + } + + err = corsPersister.Create(cors) + if err != nil { + return err + } + + err = webauthnConfigPersister.Create(webauthn) + if err != nil { + return err + } + + err = relyingPartyPersister.Create(rp) + if err != nil { + return err + } + + err = auditLogConfigPersister.Create(&config.AuditLogConfig) + if err != nil { + return err + } + + return nil +} + +func NewTenantHandler(persister persistence.Persister) *TenantHandler { + return &TenantHandler{ + persister: persister, + } +} diff --git a/server/api/handler/credentials.go b/server/api/handler/credentials.go index ed2bc6f..94bb6d2 100644 --- a/server/api/handler/credentials.go +++ b/server/api/handler/credentials.go @@ -7,8 +7,6 @@ import ( "github.com/labstack/echo/v4" "github.com/teamhanko/passkey-server/api/dto/request" "github.com/teamhanko/passkey-server/api/dto/response" - auditlog "github.com/teamhanko/passkey-server/audit_log" - "github.com/teamhanko/passkey-server/config" "github.com/teamhanko/passkey-server/persistence" "github.com/teamhanko/passkey-server/persistence/models" "net/http" @@ -24,9 +22,9 @@ type credentialsHandler struct { *webauthnHandler } -func NewCredentialsHandler(cfg *config.Config, persister persistence.Persister, logger auditlog.Logger) (CredentialsHandler, error) { +func NewCredentialsHandler(persister persistence.Persister) (CredentialsHandler, error) { - webauthnHandler, err := newWebAuthnHandler(cfg, persister, logger, nil) + webauthnHandler, err := newWebAuthnHandler(persister) if err != nil { return nil, err } @@ -44,17 +42,19 @@ func (credHandler *credentialsHandler) List(ctx echo.Context) error { userId, err := uuid.FromString(requestDto.UserId) if err != nil { + ctx.Logger().Error(err) return err } credentialPersister := credHandler.persister.GetWebauthnCredentialPersister(nil) credentialModels, err := credentialPersister.GetFromUser(userId) if err != nil { + ctx.Logger().Error(err) return err } dtos := make([]*response.CredentialDto, len(credentialModels)) - for i, _ := range credentialModels { + for i := range credentialModels { dtos[i] = response.CredentialDtoFromModel(credentialModels[i]) } @@ -71,15 +71,22 @@ func (credHandler *credentialsHandler) Update(ctx echo.Context) error { credential, err := credentialPersister.Get(requestDto.CredentialId) if err != nil { + ctx.Logger().Error(err) return err } if credential == nil { - return &echo.HTTPError{ + return echo.NewHTTPError(http.StatusNotFound, &echo.HTTPError{ Code: http.StatusNotFound, Message: fmt.Sprintf("credential with id '%s' not found.", requestDto.CredentialId), Internal: nil, - } + }) + } + + h, err := GetHandlerContext(ctx) + if err != nil { + ctx.Logger().Error(err) + return err } return credHandler.persister.Transaction(func(tx *pop.Connection) error { @@ -88,10 +95,12 @@ func (credHandler *credentialsHandler) Update(ctx echo.Context) error { err = credentialPersister.Update(credential) if err != nil { + ctx.Logger().Error(err) return err } - err := credHandler.auditLog.CreateWithConnection(tx, ctx, models.AuditLogWebAuthnCredentialUpdated, &credential.UserId, nil) + err := h.auditLog.CreateWithConnection(tx, ctx, h.tenant, models.AuditLogWebAuthnCredentialUpdated, &credential.UserId, nil) if err != nil { + ctx.Logger().Error(err) return err } @@ -108,6 +117,13 @@ func (credHandler *credentialsHandler) Delete(ctx echo.Context) error { persister := credHandler.persister.GetWebauthnCredentialPersister(nil) credential, err := persister.Get(requestDto.Id) if err != nil { + ctx.Logger().Error(err) + return err + } + + h, err := GetHandlerContext(ctx) + if err != nil { + ctx.Logger().Error(err) return err } @@ -115,11 +131,13 @@ func (credHandler *credentialsHandler) Delete(ctx echo.Context) error { persister = credHandler.persister.GetWebauthnCredentialPersister(tx) err := persister.Delete(credential) if err != nil { + ctx.Logger().Error(err) return err } - err = credHandler.auditLog.CreateWithConnection(tx, ctx, models.AuditLogWebAuthnCredentialDeleted, nil, nil) + err = h.auditLog.CreateWithConnection(tx, ctx, h.tenant, models.AuditLogWebAuthnCredentialDeleted, nil, nil) if err != nil { + ctx.Logger().Error(err) return err } diff --git a/server/api/handler/login.go b/server/api/handler/login.go index b6aa092..e903e90 100644 --- a/server/api/handler/login.go +++ b/server/api/handler/login.go @@ -11,8 +11,6 @@ import ( "github.com/labstack/echo/v4" "github.com/teamhanko/passkey-server/api/dto/intern" "github.com/teamhanko/passkey-server/api/dto/response" - auditlog "github.com/teamhanko/passkey-server/audit_log" - "github.com/teamhanko/passkey-server/config" "github.com/teamhanko/passkey-server/crypto/jwt" "github.com/teamhanko/passkey-server/persistence" "github.com/teamhanko/passkey-server/persistence/models" @@ -25,8 +23,8 @@ type loginHandler struct { *webauthnHandler } -func NewLoginHandler(cfg *config.Config, persister persistence.Persister, logger auditlog.Logger, generator jwt.Generator) (WebauthnHandler, error) { - webauthnHandler, err := newWebAuthnHandler(cfg, persister, logger, generator) +func NewLoginHandler(persister persistence.Persister) (WebauthnHandler, error) { + webauthnHandler, err := newWebAuthnHandler(persister) if err != nil { return nil, err } @@ -37,21 +35,29 @@ func NewLoginHandler(cfg *config.Config, persister persistence.Persister, logger } func (lh *loginHandler) Init(ctx echo.Context) error { - options, sessionData, err := lh.webauthn.BeginDiscoverableLogin( - webauthn.WithUserVerification(protocol.UserVerificationRequirement(lh.config.Webauthn.UserVerification)), + h, err := GetHandlerContext(ctx) + if err != nil { + ctx.Logger().Error(err) + return err + } + + options, sessionData, err := h.webauthn.BeginDiscoverableLogin( + webauthn.WithUserVerification(h.config.WebauthnConfig.UserVerification), ) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to create webauthn assertion options for discoverable login: %w", err) } - err = lh.persister.GetWebauthnSessionDataPersister(nil).Create(*intern.WebauthnSessionDataToModel(sessionData, models.WebauthnOperationAuthentication)) + err = lh.persister.GetWebauthnSessionDataPersister(nil).Create(*intern.WebauthnSessionDataToModel(sessionData, h.tenant.ID, models.WebauthnOperationAuthentication)) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to store webauthn assertion session data: %w", err) } // Remove all transports, because of a bug in android and windows where the internal authenticator gets triggered, // when the transports array contains the type 'internal' although the credential is not available on the device. - for i, _ := range options.Response.AllowedCredentials { + for i := range options.Response.AllowedCredentials { options.Response.AllowedCredentials[i].Transport = nil } @@ -61,7 +67,14 @@ func (lh *loginHandler) Init(ctx echo.Context) error { func (lh *loginHandler) Finish(ctx echo.Context) error { parsedRequest, err := protocol.ParseCredentialRequestResponse(ctx.Request()) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, err) + } + + h, err := GetHandlerContext(ctx) + if err != nil { + ctx.Logger().Error(err) + return err } return lh.persister.Transaction(func(tx *pop.Connection) error { @@ -69,16 +82,25 @@ func (lh *loginHandler) Finish(ctx echo.Context) error { webauthnUserPersister := lh.persister.GetWebauthnUserPersister(tx) credentialPersister := lh.persister.GetWebauthnCredentialPersister(tx) - sessionData, err := lh.getSessionDataByChallenge(parsedRequest.Response.CollectedClientData.Challenge, sessionDataPersister) + sessionData, err := lh.getSessionDataByChallenge(parsedRequest.Response.CollectedClientData.Challenge, sessionDataPersister, h.tenant.ID) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusUnauthorized, "failed to get session data").SetInternal(err) + } sessionDataModel := intern.WebauthnSessionDataFromModel(sessionData) - webauthnUser, err := lh.getWebauthnUserByUserHandle(parsedRequest.Response.UserHandle, webauthnUserPersister) + webauthnUser, err := lh.getWebauthnUserByUserHandle(parsedRequest.Response.UserHandle, h.tenant.ID, webauthnUserPersister) + if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusUnauthorized, "failed to get user handle").SetInternal(err) + } - credential, err := lh.webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { + credential, err := h.webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { return webauthnUser, nil }, *sessionDataModel, parsedRequest) if err != nil { + ctx.Logger().Error(err) return echo.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err) } @@ -92,17 +114,21 @@ func (lh *loginHandler) Finish(ctx echo.Context) error { dbCred.LastUsedAt = &now err = credentialPersister.Update(dbCred) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to update webauthn credential: %w", err) } } err = sessionDataPersister.Delete(*sessionData) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to delete assertion session data: %w", err) } - token, err := lh.jwtGenerator.Generate(webauthnUser.UserId, base64.RawURLEncoding.EncodeToString(credential.ID)) + generator := ctx.Get("jwt_generator").(jwt.Generator) + token, err := generator.Generate(webauthnUser.UserId, base64.RawURLEncoding.EncodeToString(credential.ID)) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to generate jwt: %w", err) } @@ -110,8 +136,8 @@ func (lh *loginHandler) Finish(ctx echo.Context) error { }) } -func (lh *loginHandler) getSessionDataByChallenge(challenge string, persister persisters.WebauthnSessionDataPersister) (*models.WebauthnSessionData, error) { - sessionData, err := persister.GetByChallenge(challenge) +func (lh *loginHandler) getSessionDataByChallenge(challenge string, persister persisters.WebauthnSessionDataPersister, tenantId uuid.UUID) (*models.WebauthnSessionData, error) { + sessionData, err := persister.GetByChallenge(challenge, tenantId) if err != nil { return nil, fmt.Errorf("failed to get webauthn assertion session data: %w", err) } @@ -127,13 +153,13 @@ func (lh *loginHandler) getSessionDataByChallenge(challenge string, persister pe return sessionData, nil } -func (lh *loginHandler) getWebauthnUserByUserHandle(userHandle []byte, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, error) { +func (lh *loginHandler) getWebauthnUserByUserHandle(userHandle []byte, tenantId uuid.UUID, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, error) { userId, err := uuid.FromBytes(userHandle) if err != nil { return nil, echo.NewHTTPError(http.StatusBadRequest, "failed to parse userHandle as uuid").SetInternal(err) } - user, err := persister.GetByUserId(userId) + user, err := persister.GetByUserId(userId, tenantId) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } diff --git a/server/api/handler/registration.go b/server/api/handler/registration.go index c47e9d4..caf15a4 100644 --- a/server/api/handler/registration.go +++ b/server/api/handler/registration.go @@ -11,8 +11,6 @@ import ( "github.com/teamhanko/passkey-server/api/dto/intern" "github.com/teamhanko/passkey-server/api/dto/request" "github.com/teamhanko/passkey-server/api/dto/response" - auditlog "github.com/teamhanko/passkey-server/audit_log" - "github.com/teamhanko/passkey-server/config" "github.com/teamhanko/passkey-server/crypto/jwt" "github.com/teamhanko/passkey-server/persistence" "github.com/teamhanko/passkey-server/persistence/models" @@ -25,8 +23,8 @@ type registrationHandler struct { *webauthnHandler } -func NewRegistrationHandler(cfg *config.Config, persister persistence.Persister, logger auditlog.Logger, generator jwt.Generator) (WebauthnHandler, error) { - webauthnHandler, err := newWebAuthnHandler(cfg, persister, logger, generator) +func NewRegistrationHandler(persister persistence.Persister) (WebauthnHandler, error) { + webauthnHandler, err := newWebAuthnHandler(persister) if err != nil { return nil, err } @@ -44,43 +42,58 @@ func (r *registrationHandler) Init(ctx echo.Context) error { webauthnUser, err := models.FromRegistrationDto(dto) if err != nil { + ctx.Logger().Error(err) return err } - fmt.Println("Here3") + h, err := GetHandlerContext(ctx) + if err != nil { + ctx.Logger().Error(err) + return err + } return r.persister.Transaction(func(tx *pop.Connection) error { webauthnUserPersister := r.persister.GetWebauthnUserPersister(tx) webauthnSessionPersister := r.persister.GetWebauthnSessionDataPersister(tx) - err := webauthnUserPersister.Create(webauthnUser) + webauthnUser.Tenant = h.tenant + internalUserDto, _, err := r.GetWebauthnUser(webauthnUser.UserID, webauthnUser.Tenant.ID, webauthnUserPersister) if err != nil { - fmt.Println("Here4") - fmt.Printf("%v\n", err.Error()) + ctx.Logger().Error(err) return err } - fmt.Printf("Ipsum") + if internalUserDto == nil { + err = webauthnUserPersister.Create(webauthnUser) + if err != nil { + ctx.Logger().Error(err) + return err + } + + internalUserDto = intern.NewWebauthnUser(*webauthnUser) + } t := true - options, sessionData, err := r.webauthn.BeginRegistration( - intern.NewWebauthnUser(*webauthnUser), + options, sessionData, err := h.webauthn.BeginRegistration( + internalUserDto, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ RequireResidentKey: &t, ResidentKey: protocol.ResidentKeyRequirementRequired, - UserVerification: protocol.UserVerificationRequirement(r.config.Webauthn.UserVerification), + UserVerification: h.config.WebauthnConfig.UserVerification, }), webauthn.WithConveyancePreference(protocol.PreferNoAttestation), // don't set the excludeCredentials list, so an already registered device can be re-registered ) - err = webauthnSessionPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, models.WebauthnOperationRegistration)) + err = webauthnSessionPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, h.tenant.ID, models.WebauthnOperationRegistration)) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to create session data: %w", err) } - err = r.auditLog.CreateWithConnection(tx, ctx, models.AuditLogWebAuthnRegistrationInitSucceeded, &webauthnUser.UserID, nil) + err = h.auditLog.CreateWithConnection(tx, ctx, h.tenant, models.AuditLogWebAuthnRegistrationInitSucceeded, &webauthnUser.UserID, nil) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to create audit log: %w", err) } @@ -91,6 +104,13 @@ func (r *registrationHandler) Init(ctx echo.Context) error { func (r *registrationHandler) Finish(ctx echo.Context) error { parsedRequest, err := protocol.ParseCredentialCreationResponse(ctx.Request()) if err != nil { + ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusBadRequest, "unable to parse credential creation response").SetInternal(err) + } + + h, err := GetHandlerContext(ctx) + if err != nil { + ctx.Logger().Error(err) return err } @@ -98,14 +118,23 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { sessionDataPersister := r.persister.GetWebauthnSessionDataPersister(tx) webauthnUserPersister := r.persister.GetWebauthnUserPersister(tx) - sessionData, err := r.getSessionByChallenge(parsedRequest.Response.CollectedClientData.Challenge, sessionDataPersister) + sessionData, err := r.getSessionByChallenge(parsedRequest.Response.CollectedClientData.Challenge, h.tenant.ID, sessionDataPersister) if err != nil { + ctx.Logger().Error(err) return err } - webauthnUser, err := r.GetWebauthnUser(sessionData.UserId, webauthnUserPersister) + webauthnUser, userModel, err := r.GetWebauthnUser(sessionData.UserId, h.tenant.ID, webauthnUserPersister) + if err != nil { + ctx.Logger().Error(err) + return err + } - credential, err := r.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), parsedRequest) + if webauthnUser == nil || userModel == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + + credential, err := h.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), parsedRequest) if err != nil { errorMessage := "failed to validate attestation" errorStatus := http.StatusBadRequest @@ -116,19 +145,22 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { // need to return an error response distinguishable from other error cases. We use a dedicated/separate HTTP // status code because it seemed a bit more robust than forcing the frontend to check on a matching // (sub-)string in the error message in order to properly display the error. - var err *protocol.Error - if errors.As(err, &err) && err.Type == protocol.ErrVerification.Type && strings.Contains(err.DevInfo, "User verification") { - errorMessage = fmt.Sprintf("%s: %s: %s", errorMessage, err.Details, err.DevInfo) + var perr *protocol.Error + ctx.Logger().Error(perr) + if errors.As(err, &perr) && perr.Type == protocol.ErrVerification.Type && strings.Contains(perr.DevInfo, "User verification") { + errorMessage = fmt.Sprintf("%s: %s: %s", errorMessage, perr.Details, perr.DevInfo) errorStatus = http.StatusUnprocessableEntity } + ctx.Logger().Error(err) return echo.NewHTTPError(errorStatus, errorMessage).SetInternal(err) } flags := parsedRequest.Response.AttestationObject.AuthData.Flags - model := intern.WebauthnCredentialToModel(credential, sessionData.UserId, flags.HasBackupEligible(), flags.HasBackupState()) + model := intern.WebauthnCredentialToModel(credential, sessionData.UserId, userModel.ID, flags.HasBackupEligible(), flags.HasBackupState()) err = r.persister.GetWebauthnCredentialPersister(tx).Create(model) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to store webauthn credential: %w", err) } @@ -137,8 +169,10 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { ctx.Logger().Errorf("failed to delete attestation session data: %w", err) } - token, err := r.jwtGenerator.Generate(webauthnUser.UserId, model.ID) + generator := ctx.Get("jwt_generator").(jwt.Generator) + token, err := generator.Generate(webauthnUser.UserId, model.ID) if err != nil { + ctx.Logger().Error(err) return fmt.Errorf("failed to generate jwt: %w", err) } @@ -146,8 +180,8 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { }) } -func (r *registrationHandler) getSessionByChallenge(challenge string, persister persisters.WebauthnSessionDataPersister) (*models.WebauthnSessionData, error) { - sessionData, err := persister.GetByChallenge(challenge) +func (r *registrationHandler) getSessionByChallenge(challenge string, tenantId uuid.UUID, persister persisters.WebauthnSessionDataPersister) (*models.WebauthnSessionData, error) { + sessionData, err := persister.GetByChallenge(challenge, tenantId) if err != nil { return nil, err } @@ -159,15 +193,15 @@ func (r *registrationHandler) getSessionByChallenge(challenge string, persister return sessionData, nil } -func (r *registrationHandler) GetWebauthnUser(userId uuid.UUID, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, error) { - user, err := persister.GetByUserId(userId) +func (r *registrationHandler) GetWebauthnUser(userId uuid.UUID, tenantId uuid.UUID, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, *models.WebauthnUser, error) { + user, err := persister.GetByUserId(userId, tenantId) if err != nil { - return nil, err + return nil, nil, err } if user == nil { - return nil, fmt.Errorf("user not found") + return nil, nil, nil } - return intern.NewWebauthnUser(*user), nil + return intern.NewWebauthnUser(*user), user, nil } diff --git a/server/api/handler/webauthn.go b/server/api/handler/webauthn.go index 3ee217a..36f85e5 100644 --- a/server/api/handler/webauthn.go +++ b/server/api/handler/webauthn.go @@ -1,16 +1,13 @@ package handler import ( - "fmt" - "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/labstack/echo/v4" "github.com/teamhanko/passkey-server/api/dto/request" auditlog "github.com/teamhanko/passkey-server/audit_log" - "github.com/teamhanko/passkey-server/config" - "github.com/teamhanko/passkey-server/crypto/jwt" "github.com/teamhanko/passkey-server/persistence" - "time" + "github.com/teamhanko/passkey-server/persistence/models" + "net/http" ) type WebauthnHandler interface { @@ -18,67 +15,63 @@ type WebauthnHandler interface { Finish(ctx echo.Context) error } +type WebauthnContext struct { + tenant *models.Tenant + webauthn *webauthn.WebAuthn + config models.Config + auditLog auditlog.Logger +} + type webauthnHandler struct { - config *config.Config - persister persistence.Persister - webauthn *webauthn.WebAuthn - auditLog auditlog.Logger - jwtGenerator jwt.Generator + persister persistence.Persister +} + +func newWebAuthnHandler(persister persistence.Persister) (*webauthnHandler, error) { + return &webauthnHandler{ + persister: persister, + }, nil } -func newWebAuthnHandler(cfg *config.Config, persister persistence.Persister, logger auditlog.Logger, generator jwt.Generator) (*webauthnHandler, error) { +func GetHandlerContext(ctx echo.Context) (*WebauthnContext, error) { + ctxTenant := ctx.Get("tenant") + if ctxTenant == nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Unable to find tenant") + } + tenant := ctxTenant.(*models.Tenant) - f := false - webauthnClient, err := webauthn.New(&webauthn.Config{ - RPDisplayName: cfg.Webauthn.RelyingParty.DisplayName, - RPID: cfg.Webauthn.RelyingParty.Id, - RPOrigins: cfg.Webauthn.RelyingParty.Origins, - AttestationPreference: protocol.PreferNoAttestation, - AuthenticatorSelection: protocol.AuthenticatorSelection{ - RequireResidentKey: &f, - ResidentKey: protocol.ResidentKeyRequirementDiscouraged, - UserVerification: protocol.VerificationRequired, - }, - Debug: false, - Timeouts: webauthn.TimeoutsConfig{ - Login: webauthn.TimeoutConfig{ - Timeout: time.Duration(cfg.Webauthn.Timeout) * time.Millisecond, - Enforce: true, - }, - Registration: webauthn.TimeoutConfig{ - Timeout: time.Duration(cfg.Webauthn.Timeout) * time.Millisecond, - Enforce: true, - }, - }, - }) + ctxWebautn := ctx.Get("webauthn_client") + var webauthnClient *webauthn.WebAuthn + if ctxWebautn != nil { + webauthnClient = ctxWebautn.(*webauthn.WebAuthn) + } - if err != nil { - return nil, fmt.Errorf("failed to create webauthn instance: %w", err) + ctxAuditLog := ctx.Get("audit_logger") + var auditLogger auditlog.Logger + if ctxAuditLog != nil { + auditLogger = ctxAuditLog.(auditlog.Logger) } - return &webauthnHandler{ - config: cfg, - persister: persister, - webauthn: webauthnClient, - auditLog: logger, - jwtGenerator: generator, + return &WebauthnContext{ + tenant: tenant, + webauthn: webauthnClient, + config: tenant.Config, + auditLog: auditLogger, }, nil } func BindAndValidateRequest[I request.CredentialRequest | request.InitRegistrationDto](ctx echo.Context) (*I, error) { - fmt.Println("lorem") var requestDto I err := ctx.Bind(&requestDto) if err != nil { - fmt.Println("Here") - return nil, err + ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusBadRequest, err) } err = ctx.Validate(&requestDto) if err != nil { - fmt.Println("Here2") - return nil, err + ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusBadRequest, err) } - return &requestDto, err + return &requestDto, nil } diff --git a/server/api/handler/well_known.go b/server/api/handler/well_known.go index b23ff4c..3aad9f1 100644 --- a/server/api/handler/well_known.go +++ b/server/api/handler/well_known.go @@ -3,25 +3,25 @@ package handler import ( "github.com/labstack/echo/v4" hankoJwk "github.com/teamhanko/passkey-server/crypto/jwk" + "github.com/teamhanko/passkey-server/persistence/models" "net/http" ) -type WellKnownHandler struct { - jwkManager hankoJwk.Manager -} +type WellKnownHandler struct{} -func NewWellKnownHandler(jwkManager hankoJwk.Manager) *WellKnownHandler { - return &WellKnownHandler{ - jwkManager: jwkManager, - } +func NewWellKnownHandler() *WellKnownHandler { + return &WellKnownHandler{} } -func (h *WellKnownHandler) GetPublicKeys(c echo.Context) error { - keys, err := h.jwkManager.GetPublicKeys() +func (h *WellKnownHandler) GetPublicKeys(ctx echo.Context) error { + tenant := ctx.Get("tenant").(*models.Tenant) + manager := ctx.Get("jwk_manager").(hankoJwk.Manager) + keys, err := manager.GetPublicKeys(tenant.ID) if err != nil { + ctx.Logger().Error(err) return err } - c.Response().Header().Add("Cache-Control", "max-age=600") - return c.JSON(http.StatusOK, keys) + ctx.Response().Header().Add("Cache-Control", "max-age=600") + return ctx.JSON(http.StatusOK, keys) } diff --git a/server/api/helper/tenant_helper.go b/server/api/helper/tenant_helper.go new file mode 100644 index 0000000..6d61993 --- /dev/null +++ b/server/api/helper/tenant_helper.go @@ -0,0 +1,28 @@ +package helper + +import ( + "fmt" + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/persistence/models" + "github.com/teamhanko/passkey-server/persistence/persisters" + "net/http" +) + +func FindTenantByIdString(id string, tenantPersister persisters.TenantPersister) (*models.Tenant, error) { + tenantId, err := uuid.FromString(id) + if err != nil { + return nil, err + } + + tenant, err := tenantPersister.Get(tenantId) + if err != nil { + return nil, err + } + + if tenant == nil { + return nil, echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("no tenant with ID '%s' was found", id)) + } + + return tenant, nil +} diff --git a/server/api/middleware/api_key.go b/server/api/middleware/api_key.go index 4a1ebce..4f0d015 100644 --- a/server/api/middleware/api_key.go +++ b/server/api/middleware/api_key.go @@ -2,32 +2,55 @@ package middleware import ( "github.com/labstack/echo/v4" - "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence/models" "net/http" "strings" ) -func ApiKeyMiddleware(cfg *config.Config) echo.MiddlewareFunc { +func ApiKeyMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { apiKey := c.Request().Header.Get("apiKey") + tenant := c.Get("tenant").(*models.Tenant) - for _, key := range cfg.Secrets.ApiKeys { - if strings.TrimSpace(apiKey) != key { - errorType := "about:blank" - title := "The api key is invalid" - details := "api keys needs to be an apiKey Header and 32 byte long" - status := http.StatusUnauthorized - - return c.JSON(http.StatusUnauthorized, &HttpError{ - ErrorType: &errorType, - Title: &title, - Details: &details, - Status: &status, - }) + var foundKey *models.Secret + + for _, key := range tenant.Config.Secrets { + + if strings.TrimSpace(apiKey) == key.Key { + foundKey = &key + break } } + if foundKey == nil { + errorType := "about:blank" + title := "The api key is invalid" + details := "api keys needs to be an apiKey Header and 32 byte long" + status := http.StatusUnauthorized + + return c.JSON(http.StatusUnauthorized, &HttpError{ + ErrorType: &errorType, + Title: &title, + Details: &details, + Status: &status, + }) + } + + if !foundKey.IsAPISecret { + errorType := "about:blank" + title := "The api key is invalid" + details := "provided key is not an api key" + status := http.StatusUnauthorized + + return c.JSON(http.StatusUnauthorized, &HttpError{ + ErrorType: &errorType, + Title: &title, + Details: &details, + Status: &status, + }) + } + return next(c) } } diff --git a/server/api/middleware/audit_log.go b/server/api/middleware/audit_log.go new file mode 100644 index 0000000..803ac8b --- /dev/null +++ b/server/api/middleware/audit_log.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "github.com/labstack/echo/v4" + auditlog "github.com/teamhanko/passkey-server/audit_log" + "github.com/teamhanko/passkey-server/persistence" + "github.com/teamhanko/passkey-server/persistence/models" +) + +func AuditLogger(persister persistence.Persister) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + auditLogConfig := ctx.Get("tenant").(*models.Tenant).Config.AuditLogConfig + + auditLogger := auditlog.NewLogger(persister, auditLogConfig) + ctx.Set("audit_logger", auditLogger) + + return next(ctx) + } + } +} diff --git a/server/api/middleware/cors.go b/server/api/middleware/cors.go new file mode 100644 index 0000000..82813bd --- /dev/null +++ b/server/api/middleware/cors.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/teamhanko/passkey-server/persistence/models" +) + +func CORSWithTenant() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + corsConfig := c.Get("tenant").(*models.Tenant).Config.Cors + + var origins []string + for _, origin := range corsConfig.Origins { + origins = append(origins, origin.Origin) + } + + return middleware.CORSWithConfig(middleware.CORSConfig{ + UnsafeWildcardOriginWithAllowCredentials: corsConfig.AllowUnsafe, + AllowOrigins: origins, + ExposeHeaders: make([]string, 0), + AllowCredentials: true, + // Based on: Chromium (starting in v76) caps at 2 hours (7200 seconds). + MaxAge: 7200, + })(next)(c) + } + } +} diff --git a/server/api/middleware/http_error.go b/server/api/middleware/http_error.go index c0c04a6..5df31dc 100644 --- a/server/api/middleware/http_error.go +++ b/server/api/middleware/http_error.go @@ -15,6 +15,20 @@ type HttpError struct { Additional *map[string]string `json:"additional,omitempty"` } +const ( + AboutBlank = "about:blank" +) + +func NewHttpError(errorType string, title string, details string, status int, additional *map[string]string) *HttpError { + return &HttpError{ + ErrorType: &errorType, + Title: &title, + Details: &details, + Status: &status, + Additional: additional, + } +} + func ToHttpError(err error) *HttpError { var e *echo.HTTPError var errorMessage string @@ -25,7 +39,7 @@ func ToHttpError(err error) *HttpError { case errors.As(err, &e): errorMessage = fmt.Sprintf("%v", e.Message) var additional *map[string]string - internalErrors := make(map[string]string, 0) + internalErrors := make(map[string]string) if e.Internal != nil { internalErrors["internal"] = fmt.Sprintf("%v", e.Internal) additional = &internalErrors diff --git a/server/api/middleware/jwk.go b/server/api/middleware/jwk.go new file mode 100644 index 0000000..8d6dab0 --- /dev/null +++ b/server/api/middleware/jwk.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "github.com/labstack/echo/v4" + hankoJwk "github.com/teamhanko/passkey-server/crypto/jwk" + "github.com/teamhanko/passkey-server/crypto/jwt" + "github.com/teamhanko/passkey-server/persistence" + "github.com/teamhanko/passkey-server/persistence/models" + "net/http" +) + +func JWKMiddleware(persister persistence.Persister) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + tenant := ctx.Get("tenant").(*models.Tenant) + secrets := tenant.Config.Secrets + + var keys []string + for _, secret := range secrets { + if !secret.IsAPISecret { + keys = append(keys, secret.Key) + } + } + + jwkManager, err := hankoJwk.NewDefaultManager(keys, tenant.ID, persister.GetJwkPersister(nil)) + if err != nil { + ctx.Logger().Error(err) + return ctx.JSON(http.StatusInternalServerError, NewHttpError( + "about:blank", + "unable to initialize jwt generator", + err.Error(), + http.StatusInternalServerError, + nil, + )) + } + ctx.Set("jwk_manager", jwkManager) + + generator, err := jwt.NewGenerator(&tenant.Config.WebauthnConfig, jwkManager, tenant.ID) + if err != nil { + ctx.Logger().Error(err) + return ctx.JSON(http.StatusInternalServerError, NewHttpError( + "about:blank", + "unable to initialize jwt generator", + err.Error(), + http.StatusInternalServerError, + nil, + )) + } + + ctx.Set("jwt_generator", generator) + + return next(ctx) + } + } +} diff --git a/server/api/middleware/tenant.go b/server/api/middleware/tenant.go new file mode 100644 index 0000000..440e569 --- /dev/null +++ b/server/api/middleware/tenant.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/persistence" + "net/http" +) + +func TenantMiddleware(persister persistence.Persister) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + const errorType = "about:blank" + + tenantIdParam := ctx.Param("tenant_id") + tenantId, err := uuid.FromString(tenantIdParam) + if err != nil { + return ctx.JSON(http.StatusBadRequest, NewHttpError( + errorType, + "bad tenant id", + "tenant_id is not a valid UUID", + http.StatusBadRequest, + nil, + )) + } + + tenant, err := persister.GetTenantPersister(nil).Get(tenantId) + if err != nil || tenant == nil { + return ctx.JSON(http.StatusNotFound, NewHttpError( + errorType, + "tenant not found", + "unable to find tenant in database", + http.StatusNotFound, + nil, + )) + } + + ctx.Set("tenant", tenant) + + return next(ctx) + } + } +} diff --git a/server/api/middleware/webauthn.go b/server/api/middleware/webauthn.go new file mode 100644 index 0000000..d9b1679 --- /dev/null +++ b/server/api/middleware/webauthn.go @@ -0,0 +1,61 @@ +package middleware + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/persistence/models" + "net/http" + "time" +) + +func WebauthnMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + tenant := ctx.Get("tenant").(*models.Tenant) + cfg := tenant.Config + + var origins []string + for _, origin := range cfg.WebauthnConfig.RelyingParty.Origins { + origins = append(origins, origin.Origin) + } + + f := false + webauthnClient, err := webauthn.New(&webauthn.Config{ + RPDisplayName: cfg.WebauthnConfig.RelyingParty.DisplayName, + RPID: cfg.WebauthnConfig.RelyingParty.RPId, + RPOrigins: origins, + AttestationPreference: protocol.PreferNoAttestation, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + RequireResidentKey: &f, + ResidentKey: protocol.ResidentKeyRequirementDiscouraged, + UserVerification: protocol.VerificationRequired, + }, + Debug: false, + Timeouts: webauthn.TimeoutsConfig{ + Login: webauthn.TimeoutConfig{ + Timeout: time.Duration(cfg.WebauthnConfig.Timeout) * time.Millisecond, + Enforce: true, + }, + Registration: webauthn.TimeoutConfig{ + Timeout: time.Duration(cfg.WebauthnConfig.Timeout) * time.Millisecond, + Enforce: true, + }, + }, + }) + + if err != nil { + return ctx.JSON(http.StatusInternalServerError, NewHttpError( + AboutBlank, + "unable to create webauthn client", + err.Error(), + http.StatusInternalServerError, + nil, + )) + } + ctx.Set("webauthn_client", webauthnClient) + + return next(ctx) + } + } +} diff --git a/server/api/router/admin.go b/server/api/router/admin.go new file mode 100644 index 0000000..6405952 --- /dev/null +++ b/server/api/router/admin.go @@ -0,0 +1,75 @@ +package router + +import ( + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/teamhanko/passkey-server/api/handler/admin" + passkeyMiddleware "github.com/teamhanko/passkey-server/api/middleware" + "github.com/teamhanko/passkey-server/api/template" + "github.com/teamhanko/passkey-server/api/validators" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence" +) + +func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometheus echo.MiddlewareFunc) *echo.Echo { + main := echo.New() + main.Renderer = template.NewTemplateRenderer() + main.HideBanner = true + + rootGroup := main.Group("") + + main.HTTPErrorHandler = passkeyMiddleware.NewHTTPErrorHandler(passkeyMiddleware.HTTPErrorHandlerConfig{ + Debug: false, + Logger: main.Logger, + }) + + main.Use(middleware.RequestID()) + if cfg.Log.LogHealthAndMetrics { + main.Use(passkeyMiddleware.LoggerMiddleware()) + } else { + rootGroup.Use(passkeyMiddleware.LoggerMiddleware()) + } + + // Validator + main.Validator = validators.NewCustomValidator() + + if prometheus != nil { + main.Use(prometheus) + main.GET("/metrics", echoprometheus.NewHandler()) + } + + statusHandler := admin.NewStatusHandler(persister) + + main.GET("/", statusHandler.Status) + + healthHandler := admin.NewHealthHandler() + + health := main.Group("/health") + health.GET("/alive", healthHandler.Alive) + health.GET("/ready", healthHandler.Ready) + + tenantHandler := admin.NewTenantHandler(persister) + tenantsGroup := main.Group("/tenants") + tenantsGroup.GET("", tenantHandler.List) + tenantsGroup.POST("", tenantHandler.Create) + + singleGroup := tenantsGroup.Group("/:tenant_id") + singleGroup.GET("", tenantHandler.Get) + singleGroup.PUT("", tenantHandler.Update) + singleGroup.DELETE("", tenantHandler.Remove) + singleGroup.PUT("/config", tenantHandler.UpdateConfig) + + secretHandler := admin.NewSecretsHandler(persister) + apiKeyGroup := singleGroup.Group("/secrets/api") + apiKeyGroup.GET("", secretHandler.ListAPIKeys) + apiKeyGroup.POST("", secretHandler.CreateAPIKey) + apiKeyGroup.DELETE("/:secret_id", secretHandler.RemoveAPIKey) + + jwkKeyGroup := singleGroup.Group("/secrets/jwk") + jwkKeyGroup.GET("", secretHandler.ListJWKKeys) + jwkKeyGroup.POST("", secretHandler.CreateJWKKey) + jwkKeyGroup.DELETE("/:secret_id", secretHandler.RemoveJWKKey) + + return main +} diff --git a/server/api/router/main.go b/server/api/router/main.go index 11d76a9..347dff1 100644 --- a/server/api/router/main.go +++ b/server/api/router/main.go @@ -1,17 +1,13 @@ package router import ( - "fmt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/teamhanko/passkey-server/api/handler" passkeyMiddleware "github.com/teamhanko/passkey-server/api/middleware" "github.com/teamhanko/passkey-server/api/template" "github.com/teamhanko/passkey-server/api/validators" - auditlog "github.com/teamhanko/passkey-server/audit_log" "github.com/teamhanko/passkey-server/config" - hankoJwk "github.com/teamhanko/passkey-server/crypto/jwk" - "github.com/teamhanko/passkey-server/crypto/jwt" "github.com/teamhanko/passkey-server/persistence" ) @@ -19,7 +15,6 @@ func NewMainRouter(cfg *config.Config, persister persistence.Persister) *echo.Ec main := echo.New() main.Renderer = template.NewTemplateRenderer() main.HideBanner = true - rootGroup := main.Group("") // Error Handling main.HTTPErrorHandler = passkeyMiddleware.NewHTTPErrorHandler(passkeyMiddleware.HTTPErrorHandlerConfig{ @@ -30,42 +25,23 @@ func NewMainRouter(cfg *config.Config, persister persistence.Persister) *echo.Ec // Add Request ID to Header main.Use(middleware.RequestID()) - // Log Metrics - logMetrics(cfg.Log.LogHealthAndMetrics, main, rootGroup) - - // CORS - main.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - UnsafeWildcardOriginWithAllowCredentials: cfg.Server.Cors.UnsafeWildcardOriginAllowed, - AllowOrigins: cfg.Server.Cors.AllowOrigins, - ExposeHeaders: make([]string, 0), - AllowCredentials: true, - // Based on: Chromium (starting in v76) caps at 2 hours (7200 seconds). - MaxAge: 7200, - })) - // Validator main.Validator = validators.NewCustomValidator() - // Audit Logger - auditLogger := auditlog.NewLogger(persister, cfg.AuditLog) - - // jwk manager - jwkManager, err := hankoJwk.NewDefaultManager(cfg.Secrets.Keys, persister.GetJwkPersister(nil)) - if err != nil { - panic(err) - } - - // jwt generator - generator, err := jwt.NewGenerator(cfg, jwkManager) - if err != nil { - panic(fmt.Errorf("unable to create generator: %w", err)) - } + rootGroup := main.Group("/:tenant_id", passkeyMiddleware.TenantMiddleware(persister)) + tenantGroup := rootGroup.Group( + "", + passkeyMiddleware.CORSWithTenant(), + passkeyMiddleware.AuditLogger(persister), + passkeyMiddleware.JWKMiddleware(persister), + ) - RouteWellKnown(main, jwkManager) + logMetrics(cfg.Log.LogHealthAndMetrics, main, tenantGroup) - RouteCredentials(main, cfg, persister, auditLogger) - RouteRegistration(main, cfg, persister, auditLogger, generator) - RouteLogin(main, cfg, persister, auditLogger, generator) + RouteWellKnown(tenantGroup) + RouteCredentials(tenantGroup, persister) + RouteRegistration(tenantGroup, persister) + RouteLogin(tenantGroup, persister) return main } @@ -78,45 +54,45 @@ func logMetrics(logMetrics bool, router *echo.Echo, group *echo.Group) { } } -func RouteWellKnown(parent *echo.Echo, manager hankoJwk.Manager) { - wellKnownHandler := handler.NewWellKnownHandler(manager) +func RouteWellKnown(parent *echo.Group) { + wellKnownHandler := handler.NewWellKnownHandler() group := parent.Group("/.well-known") group.GET("/jwks.json", wellKnownHandler.GetPublicKeys) } -func RouteCredentials(parent *echo.Echo, cfg *config.Config, persister persistence.Persister, logger auditlog.Logger) { - credentialsHandler, err := handler.NewCredentialsHandler(cfg, persister, logger) +func RouteCredentials(parent *echo.Group, persister persistence.Persister) { + credentialsHandler, err := handler.NewCredentialsHandler(persister) if err != nil { panic(err) } - group := parent.Group("/credentials", passkeyMiddleware.ApiKeyMiddleware(cfg)) + group := parent.Group("/credentials", passkeyMiddleware.ApiKeyMiddleware()) group.GET("", credentialsHandler.List) - group.PATCH("/:credentialId", credentialsHandler.Update) - group.DELETE("/:credentialId", credentialsHandler.Delete) + group.PATCH("/:credential_id", credentialsHandler.Update) + group.DELETE("/:credential_id", credentialsHandler.Delete) return } -func RouteRegistration(parent *echo.Echo, cfg *config.Config, persister persistence.Persister, logger auditlog.Logger, generator jwt.Generator) { - registrationHandler, err := handler.NewRegistrationHandler(cfg, persister, logger, generator) +func RouteRegistration(parent *echo.Group, persister persistence.Persister) { + registrationHandler, err := handler.NewRegistrationHandler(persister) if err != nil { panic(err) } - group := parent.Group("/registration") - group.POST("/initialize", registrationHandler.Init, passkeyMiddleware.ApiKeyMiddleware(cfg)) + group := parent.Group("/registration", passkeyMiddleware.WebauthnMiddleware()) + group.POST("/initialize", registrationHandler.Init, passkeyMiddleware.ApiKeyMiddleware()) group.POST("/finalize", registrationHandler.Finish) } -func RouteLogin(parent *echo.Echo, cfg *config.Config, persister persistence.Persister, logger auditlog.Logger, generator jwt.Generator) { - loginHandler, err := handler.NewLoginHandler(cfg, persister, logger, generator) +func RouteLogin(parent *echo.Group, persister persistence.Persister) { + loginHandler, err := handler.NewLoginHandler(persister) if err != nil { panic(err) } - group := parent.Group("/login") + group := parent.Group("/login", passkeyMiddleware.WebauthnMiddleware()) group.POST("/initialize", loginHandler.Init) group.POST("/finalize", loginHandler.Finish) } diff --git a/server/audit_log/logger.go b/server/audit_log/logger.go index 23cf589..102c46e 100644 --- a/server/audit_log/logger.go +++ b/server/audit_log/logger.go @@ -16,8 +16,8 @@ import ( ) type Logger interface { - Create(echo.Context, models.AuditLogType, *uuid.UUID, error) error - CreateWithConnection(*pop.Connection, echo.Context, models.AuditLogType, *uuid.UUID, error) error + Create(echo.Context, *models.Tenant, models.AuditLogType, *uuid.UUID, error) error + CreateWithConnection(*pop.Connection, echo.Context, *models.Tenant, models.AuditLogType, *uuid.UUID, error) error } type logger struct { @@ -27,9 +27,9 @@ type logger struct { consoleLoggingEnabled bool } -func NewLogger(persister persistence.Persister, cfg config.AuditLog) Logger { +func NewLogger(persister persistence.Persister, cfg models.AuditLogConfig) Logger { var loggerOutput *os.File = nil - switch cfg.ConsoleOutput.OutputStream { + switch cfg.OutputStream { case config.OutputStreamStdOut: loggerOutput = os.Stdout case config.OutputStreamStdErr: @@ -40,32 +40,32 @@ func NewLogger(persister persistence.Persister, cfg config.AuditLog) Logger { return &logger{ persister: persister, - storageEnabled: cfg.Storage.Enabled, + storageEnabled: cfg.StorageEnabled, logger: zeroLog.New(loggerOutput), - consoleLoggingEnabled: cfg.ConsoleOutput.Enabled, + consoleLoggingEnabled: cfg.ConsoleEnabled, } } -func (l *logger) Create(context echo.Context, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { - return l.CreateWithConnection(l.persister.GetConnection(), context, auditLogType, user, logError) +func (l *logger) Create(context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { + return l.CreateWithConnection(l.persister.GetConnection(), context, tenant, auditLogType, user, logError) } -func (l *logger) CreateWithConnection(tx *pop.Connection, context echo.Context, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { +func (l *logger) CreateWithConnection(tx *pop.Connection, context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { if l.storageEnabled { - err := l.store(tx, context, auditLogType, user, logError) + err := l.store(tx, context, tenant, auditLogType, user, logError) if err != nil { return err } } if l.consoleLoggingEnabled { - l.logToConsole(context, auditLogType, user, logError) + l.logToConsole(context, tenant, auditLogType, user, logError) } return nil } -func (l *logger) store(tx *pop.Connection, context echo.Context, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { +func (l *logger) store(tx *pop.Connection, context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { id, err := uuid.NewV4() if err != nil { return fmt.Errorf("failed to create id: %w", err) @@ -73,6 +73,7 @@ func (l *logger) store(tx *pop.Connection, context echo.Context, auditLogType mo al := models.AuditLog{ ID: id, + Tenant: tenant, Type: auditLogType, Error: nil, MetaHttpRequestId: context.Response().Header().Get(echo.HeaderXRequestID), @@ -93,12 +94,13 @@ func (l *logger) store(tx *pop.Connection, context echo.Context, auditLogType mo return l.persister.GetAuditLogPersister(tx).Create(al) } -func (l *logger) logToConsole(context echo.Context, auditLogType models.AuditLogType, user *uuid.UUID, logError error) { +func (l *logger) logToConsole(context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) { now := time.Now() loggerEvent := zeroLogger.Log(). Str("audience", "audit"). Str("type", string(auditLogType)). AnErr("error", logError). + Str("tenant", tenant.ID.String()). Str("http_request_id", context.Response().Header().Get(echo.HeaderXRequestID)). Str("source_ip", context.RealIP()). Str("user_agent", context.Request().UserAgent()). diff --git a/server/commands/isready/root.go b/server/commands/isready/root.go index 3f0d997..8647fe6 100644 --- a/server/commands/isready/root.go +++ b/server/commands/isready/root.go @@ -24,9 +24,9 @@ func NewIsReadyCommand() *cobra.Command { log.Fatal(err) } - host, port, err := net.SplitHostPort(globalConf.Server.Address) + host, port, err := net.SplitHostPort(globalConf.Address) if err != nil { - log.Fatalf("Could not parse address %s", globalConf.Server.Address) + log.Fatalf("Could not parse address %s", globalConf.Address) } if strings.TrimSpace(host) == "" { diff --git a/server/commands/serve/admin.go b/server/commands/serve/admin.go new file mode 100644 index 0000000..749b21d --- /dev/null +++ b/server/commands/serve/admin.go @@ -0,0 +1,42 @@ +package serve + +import ( + "github.com/spf13/cobra" + "github.com/teamhanko/passkey-server/api" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence" + "log" + "sync" +) + +func NewServeAdminApiCommand() *cobra.Command { + var configFile string + + cmd := &cobra.Command{ + Use: "admin", + Short: "Start the passkey server Admin API", + Long: "Serving all endpoints for using the passkey server Admin API", + Run: func(cmd *cobra.Command, args []string) { + globalConfig, err := config.Load(&configFile) + if err != nil { + log.Fatal(err) + } + + persister, err := persistence.NewDatabase(globalConfig.Database) + if err != nil { + log.Fatal(err) + } + + var wg sync.WaitGroup + wg.Add(1) + + go api.StartAdmin(globalConfig, &wg, persister, nil) + + wg.Wait() + }, + } + + cmd.Flags().StringVar(&configFile, "config", config.DefaultConfigFilePath, "config file") + + return cmd +} diff --git a/server/commands/serve/all.go b/server/commands/serve/all.go new file mode 100644 index 0000000..33b4348 --- /dev/null +++ b/server/commands/serve/all.go @@ -0,0 +1,47 @@ +package serve + +import ( + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/spf13/cobra" + "github.com/teamhanko/passkey-server/api" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence" + "log" + "sync" +) + +func NewServeAllCommand() *cobra.Command { + var ( + configFile string + ) + + cmd := &cobra.Command{ + Use: "all", + Short: "Start the public and admin portion of the hanko server", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.Load(&configFile) + if err != nil { + log.Fatal(err) + } + + persister, err := persistence.NewDatabase(cfg.Database) + if err != nil { + log.Fatal(err) + } + var wg sync.WaitGroup + wg.Add(2) + + prometheus := echoprometheus.NewMiddleware("hanko") + + go api.StartPublic(cfg, &wg, persister) + go api.StartAdmin(cfg, &wg, persister, prometheus) + + wg.Wait() + }, + } + + cmd.Flags().StringVar(&configFile, "config", config.DefaultConfigFilePath, "config file") + + return cmd +} diff --git a/server/commands/serve/public.go b/server/commands/serve/public.go new file mode 100644 index 0000000..f6f741f --- /dev/null +++ b/server/commands/serve/public.go @@ -0,0 +1,42 @@ +package serve + +import ( + "github.com/spf13/cobra" + "github.com/teamhanko/passkey-server/api" + "github.com/teamhanko/passkey-server/config" + "github.com/teamhanko/passkey-server/persistence" + "log" + "sync" +) + +func NewServePublicCommand() *cobra.Command { + var configFile string + + cmd := &cobra.Command{ + Use: "public", + Short: "Start the passkey server REST API", + Long: "Serving all endpoints for using the passkey server REST API", + Run: func(cmd *cobra.Command, args []string) { + globalConfig, err := config.Load(&configFile) + if err != nil { + log.Fatal(err) + } + + persister, err := persistence.NewDatabase(globalConfig.Database) + if err != nil { + log.Fatal(err) + } + + var wg sync.WaitGroup + wg.Add(1) + + go api.StartPublic(globalConfig, &wg, persister) + + wg.Wait() + }, + } + + cmd.Flags().StringVar(&configFile, "config", config.DefaultConfigFilePath, "config file") + + return cmd +} diff --git a/server/commands/serve/root.go b/server/commands/serve/root.go index 8b9cc54..00bcb0b 100644 --- a/server/commands/serve/root.go +++ b/server/commands/serve/root.go @@ -2,11 +2,7 @@ package serve import ( "github.com/spf13/cobra" - "github.com/teamhanko/passkey-server/api" "github.com/teamhanko/passkey-server/config" - "github.com/teamhanko/passkey-server/persistence" - "log" - "sync" ) func NewServeCommand() *cobra.Command { @@ -14,26 +10,8 @@ func NewServeCommand() *cobra.Command { cmd := &cobra.Command{ Use: "serve", - Short: "Start the passkey server REST API", - Long: "Serving all endpoints for using the passkey server REST API", - Run: func(cmd *cobra.Command, args []string) { - globalConfig, err := config.Load(&configFile) - if err != nil { - log.Fatal(err) - } - - persister, err := persistence.NewDatabase(globalConfig.Database) - if err != nil { - log.Fatal(err) - } - - var wg sync.WaitGroup - wg.Add(1) - - go api.Start(globalConfig, &wg, persister) - - wg.Wait() - }, + Short: "Start the passkey server", + Long: "", } cmd.Flags().StringVar(&configFile, "config", config.DefaultConfigFilePath, "config file") @@ -43,6 +21,9 @@ func NewServeCommand() *cobra.Command { func RegisterCommands(parent *cobra.Command) { cmd := NewServeCommand() + cmd.AddCommand(NewServePublicCommand()) + cmd.AddCommand(NewServeAdminApiCommand()) + cmd.AddCommand(NewServeAllCommand()) parent.AddCommand(cmd) } diff --git a/server/config/audit.go b/server/config/audit.go deleted file mode 100644 index 2fd973e..0000000 --- a/server/config/audit.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -type AuditLog struct { - ConsoleOutput AuditLogConsole `yaml:"console_output" json:"console_output,omitempty" koanf:"console_output" split_words:"true"` - Storage AuditLogStorage `yaml:"storage" json:"storage,omitempty" koanf:"storage"` -} - -type AuditLogStorage struct { - Enabled bool `yaml:"enabled" json:"enabled,omitempty" koanf:"enabled" jsonschema:"default=false"` -} - -type AuditLogConsole struct { - Enabled bool `yaml:"enabled" json:"enabled,omitempty" koanf:"enabled" jsonschema:"default=true"` - OutputStream OutputStream `yaml:"output" json:"output,omitempty" koanf:"output" split_words:"true" jsonschema:"default=stdout,enum=stdout,enum=stderr"` -} - -var ( - OutputStreamStdOut OutputStream = "stdout" - OutputStreamStdErr OutputStream = "stderr" -) - -type OutputStream string diff --git a/server/config/audit_test.go b/server/config/audit_test.go deleted file mode 100644 index 4c8d414..0000000 --- a/server/config/audit_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package config - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestParseValidAuditLogConfig(t *testing.T) { - configPath := "./testdata/audit-config.yaml" - auditConfig, err := loadTestConfig[AuditLog](&configPath) - if err != nil { - t.Error(err) - } - - assert.NotNil(t, auditConfig) - assert.NotNil(t, auditConfig.ConsoleOutput) - assert.NotNil(t, auditConfig.Storage) - assert.True(t, auditConfig.ConsoleOutput.Enabled) - assert.Equal(t, OutputStreamStdOut, auditConfig.ConsoleOutput.OutputStream) - assert.True(t, auditConfig.Storage.Enabled) -} diff --git a/server/config/config.go b/server/config/config.go index 4e44fcf..584efba 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,12 +1,14 @@ package config import ( + "errors" "fmt" "github.com/kelseyhightower/envconfig" "github.com/knadh/koanf" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" "log" + "net" "strings" ) @@ -15,33 +17,32 @@ var ( ) type Config struct { - Server Server `yaml:"server" json:"server,omitempty" koanf:"server"` - Database Database `yaml:"database" json:"database,omitempty" koanf:"database"` - Secrets Secrets `yaml:"secrets" json:"secrets,omitempty" koanf:"secrets"` - Webauthn Webauthn `yaml:"webauthn" json:"webauthn,omitempty" koanf:"webauthn"` - AuditLog AuditLog `yaml:"audit_log" json:"audit_log,omitempty" koanf:"audit_log"` - Log Logger `yaml:"log" json:"log,omitempty" koanf:"log"` + Address string `yaml:"address" json:"address,omitempty" koanf:"address"` + AdminAddress string `yaml:"admin_address" json:"admin_address,omitempty" koanf:"admin_address"` + Database Database `yaml:"database" json:"database,omitempty" koanf:"database"` + Log Logger `yaml:"log" json:"log,omitempty" koanf:"log"` } func (c *Config) Validate() error { - err := c.Server.Validate() - if err != nil { - return fmt.Errorf("failed to validate server config: %w", err) + if len(strings.TrimSpace(c.Address)) == 0 { + return errors.New("field Address must not be empty") } - err = c.Database.Validate() - if err != nil { - return fmt.Errorf("failed to validate database config: %w", err) + if _, _, err := net.SplitHostPort(c.Address); err != nil { + return errors.New("field Address must be formatted as 'host%zone:port', '[host]:port' or '[host%zone]:port'") } - err = c.Secrets.Validate() - if err != nil { - return fmt.Errorf("failed to validate secrets: %w", err) + if len(strings.TrimSpace(c.AdminAddress)) == 0 { + return errors.New("field AdminAddress must not be empty") + } + + if _, _, err := net.SplitHostPort(c.AdminAddress); err != nil { + return errors.New("field AdminAddress must be formatted as 'host%zone:port', '[host]:port' or '[host%zone]:port'") } - err = c.Webauthn.Validate() + err := c.Database.Validate() if err != nil { - return fmt.Errorf("failed to validate webauthn config: %w", err) + return fmt.Errorf("failed to validate database config: %w", err) } return nil @@ -81,27 +82,15 @@ func Load(configFile *string) (*Config, error) { func NewConfig() *Config { return &Config{ - AuditLog: AuditLog{ - ConsoleOutput: AuditLogConsole{ - Enabled: true, - OutputStream: OutputStreamStdOut, - }, - }, - Server: Server{ - Address: ":8000", - }, + Address: ":8000", + AdminAddress: ":8001", Database: Database{ - Database: "hanko-passkey", - }, - Secrets: Secrets{}, - Webauthn: Webauthn{ - RelyingParty: RelyingParty{ - Id: "localhost", - DisplayName: "Hanko Passkey Service", - Origins: []string{"http://localhost:8000"}, - }, - UserVerification: "preferred", - Timeout: 60000, + Database: "passkey", }, } } + +const ( + OutputStreamStdOut = "stdout" + OutputStreamStdErr = "stderr" +) diff --git a/server/config/config.yaml b/server/config/config.yaml index 68e8bc6..c170e92 100644 --- a/server/config/config.yaml +++ b/server/config/config.yaml @@ -1,7 +1,7 @@ database: - url: postgres://postgres@localhost:5432/hanko -secrets: - api_keys: - - 2d92ff02-a646-4ddb-948a-931f122b4371 - keys: - - super-long-and-super-secret + database: passkey + dialect: postgres + host: postgresd + port: 5432 + user: hanko + password: hanko diff --git a/server/config/config_test.go b/server/config/config_test.go index 8137b96..b780e2e 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -6,9 +6,6 @@ import ( "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "os" - "reflect" "testing" ) @@ -70,8 +67,7 @@ func TestLoadingMinimalConfig(t *testing.T) { } assert.NotNil(t, cfg) - assert.Equal(t, "60eb263e-1bfb-4e6b-818f-fdb9668f1504", cfg.ApiKey) - assert.Equal(t, defaultConfig.Server.Address, cfg.Server.Address) + assert.Equal(t, defaultConfig.Address, cfg.Address) } func TestMissingApiKeyFailure(t *testing.T) { @@ -83,16 +79,5 @@ func TestMissingApiKeyFailure(t *testing.T) { // then assert.NotNil(t, err) - assert.Equal(t, "api key needs to be defined and at least 32 bytes long", err.Error()) -} - -func TestEnvironmentVariables(t *testing.T) { - err := os.Setenv("WEBAUTHN_RELYING_PARTY_ORIGINS", "https://hanko.io,https://auth.hanko.io") - require.NoError(t, err) - - configPath := "./config.yaml" - cfg, err := Load(&configPath) - require.NoError(t, err) - - assert.True(t, reflect.DeepEqual([]string{"https://hanko.io", "https://auth.hanko.io"}, cfg.Webauthn.RelyingParty.Origins)) + assert.Equal(t, "at least one api key must be defined", err.Error()) } diff --git a/server/config/secrets.go b/server/config/secrets.go deleted file mode 100644 index a38c769..0000000 --- a/server/config/secrets.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import "errors" - -type Secrets struct { - ApiKeys []string `yaml:"api_keys" json:"api_keys" koanf:"api_keys"` - Keys []string `yaml:"keys" json:"keys" koanf:"keys" jsonschema:"minItems=1"` -} - -func (secrets *Secrets) Validate() error { - if len(secrets.ApiKeys) == 0 { - return errors.New("at least one api key must be defined") - } - - for _, apiKey := range secrets.ApiKeys { - if len(apiKey) < 32 { - return errors.New("all api keys must be at least 32 characters long") - } - } - - if len(secrets.Keys) == 0 { - return errors.New("at least one secret key must be defined") - } - - return nil -} diff --git a/server/config/secrets_test.go b/server/config/secrets_test.go deleted file mode 100644 index 7c08f4e..0000000 --- a/server/config/secrets_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestParseValidSecretsConfig(t *testing.T) { - // given - configPath := "./testdata/secrets-config.yaml" - - // when - secretConfig, err := loadTestConfig[Secrets](&configPath) - - // then - if err != nil { - t.Error(err) - } - assert.NotNil(t, secretConfig) - assert.Equal(t, 1, len(secretConfig.Keys)) - assert.Equal(t, "super-long-and-super-secret", secretConfig.Keys[0]) -} - -func TestSecretsValidate(t *testing.T) { - // given - secretsConfig := &Secrets{Keys: []string{"super-long-and-super-strong"}} - - // when - err := secretsConfig.Validate() - - // then - assert.Nil(t, err) -} - -func TestSecretsValidateWithoutKeys(t *testing.T) { - // given - secretsConfig := &Secrets{Keys: make([]string, 0)} - - // when - err := secretsConfig.Validate() - - // then - assert.NotNil(t, err) - assert.Equal(t, "at least one secret key must be defined", err.Error()) -} diff --git a/server/config/server.go b/server/config/server.go deleted file mode 100644 index 9b0f237..0000000 --- a/server/config/server.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "net" - "strings" -) - -type Server struct { - Address string `yaml:"address" json:"address,omitempty" koanf:"address"` - Cors Cors `yaml:"cors" json:"cors,omitempty" koanf:"cors"` -} - -func (s *Server) Validate() error { - if len(strings.TrimSpace(s.Address)) == 0 { - return errors.New("field Address must not be empty") - } - - if _, _, err := net.SplitHostPort(s.Address); err != nil { - return errors.New("field Address must be formatted as 'host%zone:port', '[host]:port' or '[host%zone]:port'") - } - - if err := s.Cors.Validate(); err != nil { - return err - } - return nil -} - -type Cors struct { - AllowOrigins []string `yaml:"allow_origins" json:"allow_origins" koanf:"allow_origins" split_words:"true"` - UnsafeWildcardOriginAllowed bool `yaml:"unsafe_wildcard_origin_allowed" json:"unsafe_wildcard_origin_allowed,omitempty" koanf:"unsafe_wildcard_origin_allowed" split_words:"true" jsonschema:"default=false"` -} - -func (cors *Cors) Validate() error { - for _, origin := range cors.AllowOrigins { - if origin == "*" && !cors.UnsafeWildcardOriginAllowed { - return fmt.Errorf("found wildcard '*' origin in server.cors.allow_origins, if this is intentional set server.cors.unsafe_wildcard_origin_allowed to true") - } - } - - return nil -} diff --git a/server/config/server_test.go b/server/config/server_test.go deleted file mode 100644 index 5c60801..0000000 --- a/server/config/server_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package config - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestParseValidServerConfig(t *testing.T) { - // given - configPath := "./testdata/server-config.yaml" - - // when - serverConfig, err := loadTestConfig[Server](&configPath) - - // then - if err != nil { - t.Error(err) - } - assert.NotNil(t, serverConfig) - assert.Equal(t, "http://localhost:8000", serverConfig.Address) - assert.False(t, serverConfig.Cors.UnsafeWildcardOriginAllowed) - assert.Len(t, serverConfig.Cors.AllowOrigins, 2) - assert.Equal(t, "localhost:8000", serverConfig.Cors.AllowOrigins[0]) - assert.Equal(t, "localhost:8888", serverConfig.Cors.AllowOrigins[1]) -} - -func TestValidateServerConfig(t *testing.T) { - // given - serverConfig := &Server{ - Address: "localhost:8000", - Cors: Cors{ - AllowOrigins: []string{"localhost:8999"}, - UnsafeWildcardOriginAllowed: false, - }, - } - - // when - err := serverConfig.Validate() - - // then - assert.Nil(t, err) -} - -func TestFailingValidation(t *testing.T) { - tests := []struct { - name string - address string - cors string - expectedError string - }{ - { - name: "error on empty address", - address: "", - cors: "localhost:8000", - expectedError: "field Address must not be empty", - }, - { - name: "error on wrong address", - address: "plappor", - cors: "localhost:8000", - expectedError: "field Address must be formatted as 'host%zone:port', '[host]:port' or '[host%zone]:port'", - }, - { - name: "error on cors wildcard", - address: "localhost:8000", - cors: "*", - expectedError: "found wildcard '*' origin in server.cors.allow_origins, if this is intentional set server.cors.unsafe_wildcard_origin_allowed to true", - }, - } - - for _, testData := range tests { - t.Run(testData.name, func(t *testing.T) { - // given - serverConfig := &Server{ - Address: testData.address, - Cors: Cors{ - AllowOrigins: []string{testData.cors}, - UnsafeWildcardOriginAllowed: false, - }, - } - - // when - err := serverConfig.Validate() - - // then - assert.NotNil(t, err) - assert.Equal(t, testData.expectedError, err.Error()) - }) - } -} diff --git a/server/config/testdata/audit-config.yaml b/server/config/testdata/audit-config.yaml deleted file mode 100644 index 60d763b..0000000 --- a/server/config/testdata/audit-config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -console_output: - enabled: true - output: stdout -storage: - enabled: true diff --git a/server/config/testdata/secrets-config.yaml b/server/config/testdata/secrets-config.yaml deleted file mode 100644 index 540fc12..0000000 --- a/server/config/testdata/secrets-config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -keys: - - "super-long-and-super-secret" diff --git a/server/config/testdata/server-config.yaml b/server/config/testdata/server-config.yaml deleted file mode 100644 index 05065d8..0000000 --- a/server/config/testdata/server-config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -address: http://localhost:8000 -cors: - allow_origins: - - localhost:8000 - - localhost:8888 - unsafe_wildcard_origin_allowed: false diff --git a/server/config/testdata/webauthn-config.yaml b/server/config/testdata/webauthn-config.yaml deleted file mode 100644 index 43905ff..0000000 --- a/server/config/testdata/webauthn-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -timeout: 1000 -user_verification: preferred -relying_party: - id: localhost - display_name: Hanko Passkey Service - icon: lorem - origins: - - "localhost:8000" diff --git a/server/config/webauthn.go b/server/config/webauthn.go deleted file mode 100644 index f2e6871..0000000 --- a/server/config/webauthn.go +++ /dev/null @@ -1,48 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "golang.org/x/exp/slices" - "strings" -) - -type Webauthn struct { - RelyingParty RelyingParty `yaml:"relying_party" json:"relying_party,omitempty" koanf:"relying_party" split_words:"true"` - Timeout int `yaml:"timeout" json:"timeout,omitempty" koanf:"timeout" jsonschema:"default=60000"` - UserVerification string `yaml:"user_verification" json:"user_verification,omitempty" koanf:"user_verification" split_words:"true" jsonschema:"default=preferred,enum=required,enum=preferred,enum=discouraged"` -} - -func (w *Webauthn) Validate() error { - validUv := []string{ - "required", - "preferred", - "discouraged", - } - - if !slices.Contains(validUv, w.UserVerification) { - return fmt.Errorf("expected user_verification to be one of [%s], got: '%s'", strings.Join(validUv, ", "), w.UserVerification) - } - - err := w.RelyingParty.Validate() - if err != nil { - return err - } - - return nil -} - -type RelyingParty struct { - Id string `yaml:"id" json:"id,omitempty" koanf:"id" jsonschema:"default=localhost"` - DisplayName string `yaml:"display_name" json:"display_name,omitempty" koanf:"display_name" split_words:"true" jsonschema:"default=Hanko Passkey Service"` - Icon string `yaml:"icon" json:"icon,omitempty" koanf:"icon"` - Origins []string `yaml:"origins" json:"origins,omitempty" koanf:"origins" jsonschema:"minItems=1"` -} - -func (r *RelyingParty) Validate() error { - if len(r.Origins) == 0 { - return errors.New("at least one origin must be defined") - } - - return nil -} diff --git a/server/config/webauthn_test.go b/server/config/webauthn_test.go deleted file mode 100644 index efbf57a..0000000 --- a/server/config/webauthn_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package config - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestParseValidWebauthnConfig(t *testing.T) { - // given - configPath := "./testdata/webauthn-config.yaml" - - // when - webauthnConfig, err := loadTestConfig[Webauthn](&configPath) - - // then - if err != nil { - t.Error(err) - } - assert.NotNil(t, webauthnConfig) - assert.Equal(t, 1000, webauthnConfig.Timeout) - assert.Equal(t, "preferred", webauthnConfig.UserVerification) - assert.Equal(t, "localhost", webauthnConfig.RelyingParty.Id) - assert.Equal(t, "Hanko Passkey Service", webauthnConfig.RelyingParty.DisplayName) - assert.Equal(t, "lorem", webauthnConfig.RelyingParty.Icon) - assert.Equal(t, "localhost:8000", webauthnConfig.RelyingParty.Origins[0]) - assert.Len(t, webauthnConfig.RelyingParty.Origins, 1) -} - -func TestValidWebauthnConfig(t *testing.T) { - // given - webauthnConfig := &Webauthn{ - RelyingParty: RelyingParty{ - Id: "localhost", - DisplayName: "Hanko Passkey Service", - Icon: "Icon", - Origins: []string{"localhost"}, - }, - Timeout: 1000, - UserVerification: "preferred", - } - - // when - err := webauthnConfig.Validate() - - // then - assert.Nil(t, err) -} - -func TestInvalidateWebauthnConfig(t *testing.T) { - tests := []struct { - name string - userVerification string - origins []string - expectedError string - }{ - { - name: "error on nonexistent user verification", - userVerification: "none", - origins: []string{"localhost"}, - expectedError: "expected user_verification to be one of", - }, - { - name: "error on empty origins", - userVerification: "preferred", - origins: make([]string, 0), - expectedError: "at least one origin must be defined", - }, - } - - for _, testData := range tests { - // given - webauthnConfig := &Webauthn{ - RelyingParty: RelyingParty{ - Origins: testData.origins, - }, - Timeout: 1000, - UserVerification: testData.userVerification, - } - - // when - err := webauthnConfig.Validate() - - // then - assert.NotNil(t, err) - assert.Contains(t, err.Error(), testData.expectedError) - } -} diff --git a/server/crypto/aes_gcm/aes_gcm.go b/server/crypto/aes_gcm/aes_gcm.go index 5dbc0fe..45791b8 100644 --- a/server/crypto/aes_gcm/aes_gcm.go +++ b/server/crypto/aes_gcm/aes_gcm.go @@ -16,12 +16,12 @@ type AESGCM struct { keys [][32]byte } -// Construct a AES GCM encrypter/decrypter and check the keys as a prerequisite +// NewAESGCM constructs an AES GCM encrypter/decrypter and check the keys as a prerequisite func NewAESGCM(keys []string) (*AESGCM, error) { if len(keys) < 1 { return nil, errors.New("at least one encryption key must be provided") } - hashedKeys := [][32]byte{} + var hashedKeys [][32]byte for i, v := range keys { if len(v) < 16 { diff --git a/server/crypto/jwk/generator_rsa.go b/server/crypto/jwk/generator_rsa.go index cce1dc3..6487c5b 100644 --- a/server/crypto/jwk/generator_rsa.go +++ b/server/crypto/jwk/generator_rsa.go @@ -7,7 +7,6 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" ) -// RSAKeyGenerator type RSAKeyGenerator struct { } diff --git a/server/crypto/jwk/manager.go b/server/crypto/jwk/manager.go index 2284cd3..2d8e889 100644 --- a/server/crypto/jwk/manager.go +++ b/server/crypto/jwk/manager.go @@ -12,11 +12,11 @@ import ( type Manager interface { // GenerateKey is used to generate a jwk Key - GenerateKey() (jwk.Key, error) + GenerateKey(tenantId uuid.UUID) (*models.Jwk, error) // GetPublicKeys returns all Public keys that are persisted - GetPublicKeys() (jwk.Set, error) + GetPublicKeys(tenantId uuid.UUID) (jwk.Set, error) // GetSigningKey returns the last added private key that is used for signing - GetSigningKey() (jwk.Key, error) + GetSigningKey(tenantId uuid.UUID) (jwk.Key, error) } type DefaultManager struct { @@ -24,8 +24,8 @@ type DefaultManager struct { persister persisters.JwkPersister } -// Returns a DefaultManager that reads and persists the jwks to database and generates jwks if a new secret gets added to the config. -func NewDefaultManager(keys []string, persister persisters.JwkPersister) (Manager, error) { +// NewDefaultManager returns a DefaultManager that reads and persists the jwks to database and generates jwks if a new secret gets added to the config. +func NewDefaultManager(keys []string, tenantId uuid.UUID, persister persisters.JwkPersister) (Manager, error) { encrypter, err := aes_gcm.NewAESGCM(keys) if err != nil { return nil, err @@ -34,23 +34,27 @@ func NewDefaultManager(keys []string, persister persisters.JwkPersister) (Manage encrypter: encrypter, persister: persister, } - // for every key we should check if a jwk with index exists and create one if not. - for i := range keys { - j, err := persister.Get(i + 1) - if j == nil && err == nil { - _, err := manager.GenerateKey() + + foundKeys, err := persister.GetAllForTenant(tenantId) + if err != nil { + return nil, err + } + + if len(keys) > len(foundKeys) { + keysToCreate := len(keys) - len(foundKeys) + + for i := 0; i < keysToCreate; i++ { + _, err := manager.GenerateKey(tenantId) if err != nil { return nil, err } - } else if err != nil { - return nil, err } } return manager, nil } -func (m *DefaultManager) GenerateKey() (jwk.Key, error) { +func (m *DefaultManager) GenerateKey(tenantId uuid.UUID) (*models.Jwk, error) { rsa := &RSAKeyGenerator{} id, _ := uuid.NewV4() key, err := rsa.Generate(id.String()) @@ -65,19 +69,23 @@ func (m *DefaultManager) GenerateKey() (jwk.Key, error) { if err != nil { return nil, err } + model := models.Jwk{ + TenantID: tenantId, KeyData: encryptedKey, CreatedAt: time.Now(), } + err = m.persister.Create(model) if err != nil { return nil, err } - return key, nil + + return &model, nil } -func (m *DefaultManager) GetSigningKey() (jwk.Key, error) { - sigModel, err := m.persister.GetLast() +func (m *DefaultManager) GetSigningKey(tenantId uuid.UUID) (jwk.Key, error) { + sigModel, err := m.persister.GetLast(tenantId) if err != nil { return nil, err } @@ -93,8 +101,8 @@ func (m *DefaultManager) GetSigningKey() (jwk.Key, error) { return key, nil } -func (m *DefaultManager) GetPublicKeys() (jwk.Set, error) { - modelList, err := m.persister.GetAll() +func (m *DefaultManager) GetPublicKeys(tenantId uuid.UUID) (jwk.Set, error) { + modelList, err := m.persister.GetAllForTenant(tenantId) if err != nil { return nil, err } diff --git a/server/crypto/jwt/jwt.go b/server/crypto/jwt/jwt.go index b611072..fb1b529 100644 --- a/server/crypto/jwt/jwt.go +++ b/server/crypto/jwt/jwt.go @@ -6,8 +6,8 @@ import ( "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/teamhanko/passkey-server/config" hankoJwk "github.com/teamhanko/passkey-server/crypto/jwk" + "github.com/teamhanko/passkey-server/persistence/models" "time" ) @@ -21,18 +21,18 @@ type Generator interface { type generator struct { signatureKey jwk.Key verKeys jwk.Set - config *config.Config + config *models.WebauthnConfig } // NewGenerator returns a new jwt generator which signs JWTs with the given signing key and verifies JWTs with the given verificationKeys -func NewGenerator(cfg *config.Config, jwkManager hankoJwk.Manager) (Generator, error) { - signatureKey, err := jwkManager.GetSigningKey() +func NewGenerator(cfg *models.WebauthnConfig, jwkManager hankoJwk.Manager, tenantId uuid.UUID) (Generator, error) { + signatureKey, err := jwkManager.GetSigningKey(tenantId) const jwkGenFailure = "failed to create jwk jwtGenerator: %w" if err != nil { return nil, fmt.Errorf(jwkGenFailure, err) } - verificationKeys, err := jwkManager.GetPublicKeys() + verificationKeys, err := jwkManager.GetPublicKeys(tenantId) if err != nil { return nil, fmt.Errorf(jwkGenFailure, err) } @@ -72,7 +72,7 @@ func (g *generator) Generate(userId uuid.UUID, credentialId string) (string, err token := jwt.New() _ = token.Set(jwt.SubjectKey, userId.String()) _ = token.Set(jwt.IssuedAtKey, issuedAt) - _ = token.Set(jwt.AudienceKey, []string{g.config.Webauthn.RelyingParty.Id}) + _ = token.Set(jwt.AudienceKey, []string{g.config.RelyingParty.RPId}) _ = token.Set("cred", credentialId) signed, err := g.Sign(token) diff --git a/server/crypto/jwt/jwt_test.go b/server/crypto/jwt/jwt_test.go deleted file mode 100644 index cf1f0fd..0000000 --- a/server/crypto/jwt/jwt_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package jwt - -import ( - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestNewGenerator(t *testing.T) { - signatureKey := getSignatureJwk(t, key1) - require.NotEmpty(t, signatureKey) - verificationKeys := getVerificationJwks(t) - require.NotEmpty(t, verificationKeys) - - jwtGenerator, err := NewGenerator(signatureKey, verificationKeys) - assert.NoError(t, err) - require.NotEmpty(t, jwtGenerator) -} - -func TestGenerator_Sign(t *testing.T) { - signatureKey := getSignatureJwk(t, key2) - require.NotEmpty(t, signatureKey) - verificationKeys := getVerificationJwks(t) - require.NotEmpty(t, verificationKeys) - - jwtGenerator, err := NewGenerator(signatureKey, verificationKeys) - assert.NoError(t, err) - require.NotEmpty(t, jwtGenerator) - - token := jwt.New() - err = token.Set(jwt.SubjectKey, subject) - assert.NoError(t, err) - - signedTokenBytes, err := jwtGenerator.Sign(token) - assert.NoError(t, err) - require.NotEmpty(t, signedTokenBytes) -} - -func TestGenerator_Verify(t *testing.T) { - signatureKey := getSignatureJwk(t, key1) - require.NotEmpty(t, signatureKey) - verificationKeys := getVerificationJwks(t) - require.NotEmpty(t, verificationKeys) - - tests := []struct { - Name string - JWT string - WantErr bool - }{ - { - Name: "with signature key 1", - JWT: validJwt1, - WantErr: false, - }, - { - Name: "with signature key 2", - JWT: validJwt2, - WantErr: false, - }, - { - Name: "with unknown signature key", - JWT: invalidJwt, - WantErr: true, - }, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - jwtGenerator, err := NewGenerator(signatureKey, verificationKeys) - assert.NoError(t, err) - require.NotEmpty(t, jwtGenerator) - - verifiedToken, err := jwtGenerator.Verify([]byte(test.JWT)) - if test.WantErr { - assert.Error(t, err) - assert.Empty(t, verifiedToken) - } else { - assert.NoError(t, err) - require.NotEmpty(t, verifiedToken) - assert.Equal(t, subject, verifiedToken.Subject()) - } - }) - } -} - -func getSignatureJwk(t *testing.T, keyString string) jwk.Key { - key, err := jwk.ParseKey([]byte(keyString)) - require.NoError(t, err) - return key -} - -func getVerificationJwks(t *testing.T) jwk.Set { - key1 := getSignatureJwk(t, key1) - key2 := getSignatureJwk(t, key2) - set := jwk.NewSet() - - err := set.AddKey(key1) - require.NoError(t, err) - err = set.AddKey(key2) - require.NoError(t, err) - return set -} - -var key1 = `{ - "d": "rPY8WpbbnGqd35cAGyDQ6EotozrQk7mjP8E6ztm0BLRoK0B8SYmGwxxpLZGDF6eeH5WSYl3a5RGvHZemMKZlMHryO_muMloRxu8iF9tfwjULcASu-1Ivz5wlXWe7PeFlOGwp3sbS1VwsUfkmDr6VCcRX7hEtmcn8QJyPs12b1gfBagOwZN0iNJYn3YUko5m-lGc4Ca-H3GXFPxtRt89z1h8B_7IRPDNOMZK3Wo1fl79MA_idIvt-Y5EAcUoi7aW8wlW0vx1h81r0taGUP7UOasz-2wkvMOJWU3Nn6GEx_S7fXfA7WFEr5KhoNLncdfNfVUKEuVPSIqkl6BClxovYPPb-ilZxHakIsrSFhcaQ_0OJpm8njmWCIIQNtZVgbV55OLzGaI6cDEmRFoEO_fvlG0rKxaGWDRJpJfZrt5VBhvxgNTDaJtOWyQRjC2d3y3rdIY0yxU113CYK5n1SeB3V1H--ElcOWlOeSh8pV_ZL1014TTWxBGpdgzvtHp-ooWs-qZZpnj_mb2bD-KdGvCczKm8QfFxQQgu8QswnE7LBnKQ3aM46M2swUlQnutjbMvc9HhNCVtwxTwdJTjCCkO6YClctxhvEjSksTQFY1Utr2jQsNWWLieRma1WUyH82_5BF4IF4jLQwZ6fZGtcJe0YJK2dlQQ4ITqHg8E_WsfsFSuE", - "dp": "bv3Gj-UKF7QnUEXDLxbEPBPRt87tR8f-_9ELdCyDqDjZ_j1z4ua4vCZ1ldQ9bcaELG0NQVRFVDGD3EbaSAX2jWfxaKPIWuN7MV3ixvuf-bAdtPSRDzwaiflyG9icn9HWoz8TBL_VRwgTbswzXBTgqNnl2RpmCTxDcc4HeQexBSpwPNMV1pv5fHiP79GisGHBV5d5ycKaszcTkco-2Ls7KtBwL7pQBx-yCyHd9cQTuoL8ssOnkiB47KpZSohWZLbwjWmAK2-5_ufx5ByM5E60zAgbyVfO0xXxY5vjwSkbD4Vnr0tPxLScuAH3NUZ8rXr3t0aSgzKJxXKJueIiRtYVdw", - "dq": "w_msGUgSOL4RLbU0FsieYoRAjLhYjfPJ4J4Pc1UuJOca-QREkE3PAti0KWNfZBm1WIdZphuZYUupt8-UhBIHk2Z8fhejSHuoBZXGj7tSvYNQtzTyzKrh9I8n1GxUop5KUIHRW70_I4-22OyUVhQsQtJewLG3-aG0qz3iYsscamNnNUZixAidhF4s66f1piWjhVTdqdp2NUF1fp41ebmHNajKiePoMQt5NQhzBZBxT9LmhB2e3nzNvTNNkrFocR7y__umk6fNfD7RUDcQGNl5L9RmvxgHL88KMi5ApOMiiJfAcEgtWpr2LvcBMgI7OrZnGodaYPE1ZG_sH_pqPYnAwQ", - "e": "AQAB", - "kid": "key1", - "kty": "RSA", - "alg": "RS256", - "n": "ugADkMwTn1PIegp1VT9iOnX0L2-DhTL-6NO5LWx6GQMu_b87hODWL2v8u1fklUTNLEtmxubQY7nMsLUl0_zzHKUQLjk69vC5CMdq2yMYQ--sv2YViFQX4k9FMgNnveJunFQj4siA7VDIApxOYjSVMQ6HXQUCjclxMoQlLw6TvDd2h1dP6UXDvqgAp2iM8XaSiTWFtMgKkYreIB7aXwPgLrkffJ7wh7d9duLlygIIogGAucnxPLs8qn5Hpb9RwZs8kcXVgFWhZWlfnQaMXaVzjhN0uTcPD0JLpPQ9XLVtbsDwIRDYhTrZBX_58GQDlAJS-3M4LtkP3AXMnUhuWyK4vwCgoEV5VmPkWTF1DnwP2bjzRRzs0f9c6r6JYSEkZfIHksR8ThADnIUh1r89sRN5na5h9kC3g_9yLwUR3K64s1turZMZQsJSPxnXL-jMkaUKqgujMhH8A97aPnSRQ-SW7PHUGXE23tBEk_2rslsHOA22fo3Z09VyGwJD6BXwFwW9pj3tU4LNg0UyOk5QexSEOrVJS3dL3X9cXDgEqfhXlEZeFaA0882nxv2kwyUBsz5Rq2pHh7Pj4v0izWz-he4VyWg-8-HoNK3Oy4wGdhStTa2cU0CFNcu0VqTrVmYkpsEPNWmVapBlP-2pmiRLQLUYZVsjit9gIM4VwEPcna0wNcc", - "p": "53kTdYIfPUEv_ml9vw6A0UPAdIf0plOvnCiHyzYQ7yY3Z37IfDy8h9esWvYH6xy7W65jECaBUcWn88UbMQ0lEaxsvh0XNunVmMtNX41bC-cRMH9kqcewvIHSRtjHbUmXCNpPETyNYRuUS7F0MbHh2y7SrG2krRSXaMABGamVZ0iWaKSUHXCCq-vuPnaEhoRDK3OI-aQQTr24I8LXmAECYyNjTC5NUiNAMSu7A3I7id31Pqgcht8v0gQSpWatJtg7RqNAA4S1QxnrjZRcpcq3IvrTWGB0WpgQb4z3yfKXxaz7XtLm8zSD2JfoRYTLM_qqbJwhvYzmSCFtDxxC1z8g9w", - "q": "zbVxOg87-yWEOOd0k6jE2u25xpnUCfTpkLycApPXwtBSh4AQWpuTO4zUXbZUsQjg9mRZV1Ksnz1Vvb8BRx_Q60-G6lGVTANp1uP88HGA7hlIYxWCeZYDLfF0P0Iueb0a4D-kigX7UAwt5FzAbM4RtW-2VxcUIzP3lxxpsILw0arBVdkpv_wzjT4Mqz6KpQFmocdt9kqO6lVLn9dG90l-7CN9_El77aVpR13cqh-M7uDhgcN-SVwCZxmDvUcDC2Vj5JKEz4_SwBqPLMrH3Ba1ASeRvEJ7VVN4y_6zE6Wx4cQsk9bba9Yl_fzRo5yqW-JKCXksO_F2Sg4S_eDY-3ktsQ", - "qi": "tkb9_3gwcIOmQqncjaT-A-vmKotEZs6U186u0CsQIKNvpcwMOWHjdRxd896tj9wE5F0JKxllwRqN2o0bb6QlmM56_C3Agh3MQ5CFiDC6Q7PEcSMbTihfRFUeNGP4gpiTaUg67HpdHWkYnsziYG6uTUh9onyntg-uZz3xMlOIEfURKCSYKamXgBQ_6EZyDG4xQv4Og_HUbCao4XThkiH4t70tkGi8hmagE0D7eiUY0PQul4x3Z9J9_xGdY3AxqvnLGnKbJw6m0g73fRcL3IixDfqFGuYIYvbdQCxgzURW3LGKyCsWx8A0CjVu9dU3XiNn86ZEVr4ZwxT9_XSbS1l3bA" -}` - -var key2 = `{ - "d": "mF2NW0lRf03XFc7TIDZqscl-i2z5_-2jSc99SOACWn4NiM_Zh4VlVqe-hh1lJsE7bro8Kbe4L_h4ca5Ef_cHgd6oGjv5DPJidNDrcxtUpQoeJnTZuOcpAybseFjDLQCqhPca9zF8aeIEUq0F38YZqfu1d-94r10MtibgBBOvOpqkLlvjFVfxc_-fzpxIOnrrFC0Jtp1sjFbwTPeIV7HUmc8jkM1E8zXqyj9yQBHoFHUbEQHfK2Cdu_UXedzhKfpQovB1-y3-6Fw6VDbE1BChJ8BAp6zC2J7FdHOL47vqjMVE-io_NJcAm1P82hvFFfuitU1Nfi4rY8XWRuCwwhLp-aifQ5ugAUVjChzOCrka8eiN-BSlwmgiv-tql32DP7wYiY9ixGwdXRU7I4WH51O2L36Cqrq5Rrd0F0WfzRBmAiq4YPPjZp2w3p2pyvRz9jlEzE4BQXrdrcE6JY5Vb2htysSml3_aLXg--CgiO5D8R6IM8Oxh2bcE1cuZo67KFnO1_iOhyoNGXw9usclierniaHmdUVUwBujMzT3s9fE1zpEWccE1VpEzQ_8QUFxrlevmxMTLw5RolY71PruRuTXr135K8tMviOpuftWB05OtBhMKFWXTnq-qRMaSfp8nmHYpVk1qwDAe24f2s56ClkqiKkQnZsEgu5m_mEfcc_2Bi_U", - "dp": "EBGwGpFQdAjGryNDr3heh1mAbLASvde9ARN8vmDRP9pMEDJOpnAatai-EYh38PpvUYKb6W_JLtwOshRa7U-6sjmZGaMLH5Ftbh4YF3nJ21RhyUczaP3CyOMaueeFPAP80e15RrZubZID5aIKF2H26wWvprzKGsh956uUI6AxtZUGdW4-N9_EzJkSBUqAlk7aO6KKZKYqoavUNZfYxst4dvy97-QI1s1dUFxPPTC6ltPk25Zv-n7nC4y9-Gb9GOO3oh3mfD2paQBX8blHwQU78ZZ2z2WCuXaGBUY41ybArFnFj8W8KhLPsocPoSkeuxSuOQnG7UW1qVWG3Nhxtupwnw", - "dq": "q0s0U-b24CMeEnzKmyoYDRuIyUfmLzT7nnOaIxT_57tFt-7P9Nbp_HsMoAUoiV8eXP1byrxgqVLfJcsyDI408xSunj9wxCDSWwujg-aSf1gJ-mO9mtWTtttaDBGWeMgxIoA5DCpOzBQXXZ9CeaW95uzuWPrMr3xflYL__DSZ9wrrBs9aF2Ej3KspQYkg3kPUBC3SGvj5onXtbD9YV3mq-cpAyS2aD15UpIS4IwFuDT-iOe-23nc-wAc2rrC1RXNahrm9f7sXZrdEycOe8RQss3G0MBz8BEuQRvfegvqrRxjxHJ8P4CFnNDvu4vqsc-OWmaAzYqeonfHyVHWoRhzumQ", - "e": "AQAB", - "kid": "key2", - "kty": "RSA", - "alg": "RS256", - "n": "sCi1KDOdUKttWUxSwA_yPE9cI4yb3d_Cio9ig2G2lk89ugOvzslqr6OtKdnjnKeYtKMMIDDbDTsa7kJVpWBHNxycIzoB1nUjXNYfYzqIfWZdXsbUlbTeTA-yUXyRW13CJQfxk59jUlYm1VLXxyZvScmBVmKlNLK7oDgHT72uYoGnem35w6Hb85gpuLnYR3E0iq7-H8yQ3oBKJ8TOyJwZSfzMujuz7njaZ0uW84phkb9H6We9-p_w9GSCeheY4Zu1Ercr77SbcI_hODxpyCjQUwgr7uGKae4P4moa1PkyBRWb2wSZYeqW81NkZlbu1TZfnKTaeQFSQMbUb4JNyk0Sz1lgfpgG6rdE1OTyIJmer6I9gRSgNmyxF9rGD-fpMk0ZPouLcbkx6KBmefWp-gv2l34-uW2czoXc7c5pmfPvWjxd1wLpES5SLca-zF5MNOhrtnqgZ-ComjajwgEztX_XZCy5vYK6qDnLOhognWOut0qJn1WMLgLvMgc9yfdZrNnZ803-zkiRIWR1uFkxw8uH2G-a2MqR6EJ8liN9HzSIUwwI5NTkkYyAndf69Rn-DuSPWpLnNNfgVz06fhdC6k6S-aGHrIrzLClLTnX9ni5j6ypdZcbA4iT_fcgBKD8BGJlV1QMzHvLCn8MKBoCNdgVBRS1vXTwwa61-7Cm3j9eWloE", - "p": "ybVhcMu7a5bPmbcUYnXf6zYrjOU_yZjgae9R7ViVfBRkvMtU0G0a6EpR_wuxbCK6z-UzJLXxIC1h7NSnxhNRvEBMQeg1FVszab2Fl9k7PMNmtPqwbeQKVCKABmLn5vSErbA71D-aNUV0CxQKdvTJR19NvNjEHCyOM_MLxyNrBww2DnM1slTcE7Rr_OM2OI5q6lEA5FLtmtGof0qx7BrVabmJRhEASUPRZ8ir4mH05ZhUT5g5tVE5iZ_fTV8l5XbU_Xn5VqXo45kT3I9xuME8f2g2z9EIVZMGHoKg_ip0KeVSZtYS6tEQfdrwXpJoDq5TibuliXhSuGvht0yqaA4ZRw", - "q": "35LaFXiFAIUerhgCPjlnXujNQ9pPXfgaCibp8qiBr6pqbgabYGT7l4d62IwwHwmFvcfhiFTlK7iqc9jiaGAaHDujrEuIWVTua4uWHUu2iCj_a8SnoKf0J1BgIdS85Z_VeQrjSbLtdDzKUARnzsVoNt9_y60RaEcL27Zyut5xfPnfQb2P_gSVrWa2LdDnOAESdMhgyq_GYqljWRU2V75DhS0P_FIpYifzc72AAkBTm1Ni7-DsoyN9rn8_wofhxHQ-NfuoRkd0WrQiL1Co9scSAWtevLpLqqFq76pgBvsJFYlkbPviDuTP4S5UYgTurbZZ69hW-PrQ-w60g_Iyg3u19w", - "qi": "ttLq3IfagjTOoCLVuPKwmyHgt7FxzbpD0RevGDy25gwrSbmFma_zUFRmC361qcDk9qwSEC1V1qSXFGt5TnXp_ZYyJ6V0PxMa00kMYZqj1lAZ-PiVW8FlyV_tySHqG1gzBemj46gcuPXcHmzZ59CkkhPMzCfH_3QllM9H0Ts9oaW1rIXr2PUKW5GuttgXxDQ92DF-H-tHNx73yvSBcMA_LBn9sclGl6ARFr6sCVZrlq-tDsQXoWVRgLphA23L8O0PCaluhnh8cCBtlo8XlKfLJ5gIOrK7lzKtnCgZ-rP7lPBbo3vmvWCdbjQh5W4ht5aiHPczZlmZjPpSJ5n_7kgvxg" -}` - -var subject = "c21ae0e1-39ad-494f-badd-2d54e072641e" -var validJwt1 = `eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleTEiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJjMjFhZTBlMS0zOWFkLTQ5NGYtYmFkZC0yZDU0 -ZTA3MjY0MWUifQ.YwN1KRYbAS2zhsI3mrOcH56ngMxHCKAEtGXrhAnDC0WxR577kB22upYFVWmHuAFJtd6gJRz7leHUIwy7Ed9uDgkHbOgGtxXXONbUVg-ho -l1qwJikJG_WFMFetlztY86U2lMHAwPgw8BazeEPmfSIq23KXuP1ifW33XJ_KnNloUCno44CXlsoUEpNKPIJULVLsf0UkuEzfhp0NwQJ0FcZ_Qh_g4QcJwLZ8 -xmqnCoGSN7p9zBlxvMwietlPCAqI50S4wW5I5or9MpwHpo2ejrt2JLj1H5v6LtE8-FakGPE5Cz5_84tLbWhAPO_IqaN-xMC3O2LVrGik8AdltZCXnDBKToCf -u8LEUnX0wnuBp_LlooBpC5fo6mGN45q0MBEP3n6HXQpoMLZn50KVetG7YAuaFoYZLUd6I4bhOTUDMkCSK-thTL6_uMvFDhrMzxOAKjBfClq7rcpApCsIATEx -v2xMiK0Kj4rXXgL_0I5aHtiDyDviYz2LOnoZM36Pclei-ea399uZHT1caOfsZQCHCIMtc0I4wENCz34Du8y05CN5XNC_DrALOq8D0BVSESBdsmYAUMERx-u- -qBpal9KdTAdARKZvaAPT2Tc2ZlgXL42fkSVHjSMm51yJe6MB-KAa6a9FEbVYw7ZBnTf4_0aQ9StK8HwMjkx9E37NE3YjPbtOsg` -var validJwt2 = `eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleTIiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJjMjFhZTBlMS0zOWFkLTQ5NGYtYmFkZC0yZDU0 -ZTA3MjY0MWUifQ.iMhqgKf_ztfnGEbQX2qmt3o92BtfDJA6fvL3aKFWV7RC7XChH1-1OPj2AdsC73U3t8y8Ud4bcyengZffBu3L3wspD-MeJT8ti54yMWDqE -wAcA_9iOmk5X4SSBPphXGnuZIRWCvqdIzWnWk71ONu7fZ-VywN4MdkiqL1c5AevSxCK0AH7EFF6ZDg65QcFDfjSu6QI0HiRr7KU1uMK9BjdSWuvZlwogVI41 -rE7LWqHdRSa56dQZoaHbbB-NEmGgC38eKo3BtIt6R_pCmMYNsKKxY5dHy6FM2WWmBwkYCPBxD6gowSXcPic2DTR__NlovbTCzVgEvIytHJhWlnX01d1Qd9Jk -5JOFb0bwkTpuxkwbZuxBBId7CDNBhVtnYIPhTY72yvC2lof7EFwoWVE1S_06JovgIBSlySCBN3kMNErswQQbbbJWge82WEMvOvC0GNFp8oGcHW-hygRA4u_T -GmWaQ1NUPAswqyYid32rsW2Pq6KjZcSh1c6rKaS7BTb3kMnRRiNDCn9Ibfe-2x1uWrkHur989PUa92ycTe89Tp_XSt9OXNqIE52auXmQYOOTyHrnmPTehzhv -cu1FSqGN1hfSK2RpQHjnxwrabekgqPbQA03KK0ZPTCG9lMQTd3zMsJutsfmXwfusVCwINTE0aqa4GSK4Y81ch4WAksznBH9S8A` -var invalidJwt = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjMjFhZTBlMS0zOWFkLTQ5NGYtYmFkZC0yZDU0ZTA3MjY0MWUifQ.c -WXSextdnNtzNll1MdrCG73qBM4px-pz-pCn1hCbG2g5aHLtKeKwxYAhus4i_NSVDMmuIULk9hmteUUAM3YByFtCjKZElWC9laEiYydERzatkJYi3-h1N05y -I8K2aav_3bPubThp_u0Mgwxiz10bx7Qon7BakvX27B29iETcWTAyMvrQTnQGC3Z89Z8plYeBUGkD4ftN8z3TSVUvdFvgJ8E1LnrricbL9mKigv2q9HMXqC_ -23GmhSqRGdHp48JIyVf6PZoSD0qwC8mQNM3R_kMW9cCbTWu7CrdzNwsbB_NJoH_UXwteJMY19FeljeY3ELhWdy8tOzJwSz9G3oEFbtA` diff --git a/server/crypto/string.go b/server/crypto/string.go new file mode 100644 index 0000000..034a755 --- /dev/null +++ b/server/crypto/string.go @@ -0,0 +1,31 @@ +package crypto + +import ( + "crypto/rand" + "encoding/base64" +) + +// GenerateRandomBytes returns securely generated random bytes. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} + +// GenerateRandomStringURLSafe returns a URL-safe, base64 encoded +// securely generated random string. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomStringURLSafe(n int) (string, error) { + b, err := GenerateRandomBytes(n) + return base64.URLEncoding.EncodeToString(b), err +} diff --git a/server/go.mod b/server/go.mod index 4c582ca..0c3aa3b 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,9 +1,9 @@ module github.com/teamhanko/passkey-server -go 1.20 +go 1.21 require ( - github.com/go-playground/validator/v10 v10.15.5 + github.com/go-playground/validator/v10 v10.16.0 github.com/go-webauthn/webauthn v0.8.6 github.com/gobuffalo/nulls v0.4.2 github.com/gobuffalo/pop/v6 v6.1.1 @@ -11,24 +11,26 @@ require ( github.com/gofrs/uuid v4.4.0+incompatible github.com/kelseyhightower/envconfig v1.4.0 github.com/knadh/koanf v1.5.0 - github.com/labstack/echo/v4 v4.11.1 - github.com/lestrrat-go/jwx/v2 v2.0.13 + github.com/labstack/echo-contrib v0.15.0 + github.com/labstack/echo/v4 v4.11.2 + github.com/lestrrat-go/jwx/v2 v2.0.16 github.com/rs/zerolog v1.31.0 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 - golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 ) require ( - github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/fxamacker/cbor/v2 v2.4.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect @@ -36,16 +38,16 @@ require ( github.com/gobuffalo/envy v1.10.2 // indirect github.com/gobuffalo/fizz v1.14.4 // indirect github.com/gobuffalo/flect v1.0.2 // indirect - github.com/gobuffalo/github_flavored_markdown v1.1.3 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect github.com/gobuffalo/helpers v0.6.7 // indirect - github.com/gobuffalo/plush/v4 v4.1.18 // indirect + github.com/gobuffalo/plush/v4 v4.1.19 // indirect github.com/gobuffalo/tags/v3 v3.1.4 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/google/go-tpm v0.9.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/css v1.0.0 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.1 // indirect @@ -58,7 +60,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect @@ -69,17 +71,22 @@ require ( github.com/lib/pq v1.10.9 // indirect github.com/luna-duclos/instrumentedsql v1.1.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect - github.com/microcosm-cc/bluemonday v1.0.20 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.18 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/sergi/go-diff v1.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect @@ -88,12 +95,13 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.14.0 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.4.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..f7e34e2 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,772 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= +github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-webauthn/webauthn v0.8.6 h1:bKMtL1qzd2WTFkf1mFTVbreYrwn7dsYmEPjTq6QN90E= +github.com/go-webauthn/webauthn v0.8.6/go.mod h1:emwVLMCI5yx9evTTvr0r+aOZCdWJqMfbRhF0MufyUog= +github.com/go-webauthn/x v0.1.4 h1:sGmIFhcY70l6k7JIDfnjVBiAAFEssga5lXIUXe0GtAs= +github.com/go-webauthn/x v0.1.4/go.mod h1:75Ug0oK6KYpANh5hDOanfDI+dvPWHk788naJVG/37H8= +github.com/gobuffalo/attrs v1.0.3/go.mod h1:KvDJCE0avbufqS0Bw3UV7RQynESY0jjod+572ctX4t8= +github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= +github.com/gobuffalo/fizz v1.14.4 h1:8uume7joF6niTNWN582IQ2jhGTUoa9g1fiV/tIoGdBs= +github.com/gobuffalo/fizz v1.14.4/go.mod h1:9/2fGNXNeIFOXEEgTPJwiK63e44RjG+Nc4hfMm1ArGM= +github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.0/go.mod h1:l9V6xSb4BlXwsxEMj3FVEub2nkdQjWhPvD8XTTlHPQc= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/genny/v2 v2.1.0/go.mod h1:4yoTNk4bYuP3BMM6uQKYPvtP6WsXFGm2w2EFYZdRls8= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADgMzESKNWls9hOTG1MHVs= +github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= +github.com/gobuffalo/helpers v0.6.7 h1:C9CedoRSfgWg2ZoIkVXgjI5kgmSpL34Z3qdnzpfNVd8= +github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/logger v1.0.7/go.mod h1:u40u6Bq3VVvaMcy5sRBclD8SXhBYPS0Qk95ubt+1xJM= +github.com/gobuffalo/nulls v0.4.2 h1:GAqBR29R3oPY+WCC7JL9KKk9erchaNuV6unsOSZGQkw= +github.com/gobuffalo/nulls v0.4.2/go.mod h1:EElw2zmBYafU2R9W4Ii1ByIj177wA/pc0JdjtD0EsH8= +github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= +github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.18/go.mod h1:xi2tJIhFI4UdzIL8sxZtzGYOd2xbBpcFbLZlIPGGZhU= +github.com/gobuffalo/plush/v4 v4.1.19 h1:o0E5gEJw+ozkAwQoCeiaWC6VOU2lEmX+GhtGkwpqZ8o= +github.com/gobuffalo/plush/v4 v4.1.19/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= +github.com/gobuffalo/pop/v6 v6.1.1 h1:eUDBaZcb0gYrmFnKwpuTEUA7t5ZHqNfvS4POqJYXDZY= +github.com/gobuffalo/pop/v6 v6.1.1/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI= +github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= +github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= +github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4= +github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= +github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= +github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= +github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4= +github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= +github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= +github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= +github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU= +github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4= +github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE= +github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.13/go.mod h1:UzXMzcV99p9/xe1JsIb336NJDGXLsleR+Qj3ucEDtfI= +github.com/lestrrat-go/jwx/v2 v2.0.16 h1:TuH3dBkYTy2giQg/9D8f20znS3JtMRuQJ372boS3lWk= +github.com/lestrrat-go/jwx/v2 v2.0.16/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= +github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231005195138-3e424a577f31/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/server/persistence/migrations/20231012102310_create_webauthn_users.down.fizz b/server/persistence/migrations/20231012102310_create_webauthn_users.down.fizz deleted file mode 100644 index 8e183e7..0000000 --- a/server/persistence/migrations/20231012102310_create_webauthn_users.down.fizz +++ /dev/null @@ -1,3 +0,0 @@ -drop_foreign_key("webauthn_credentials", "webauthn_user_credentials_fk", {"if_exists": true}) - -drop_table("webauthn_users") diff --git a/server/persistence/migrations/20231012102310_create_webauthn_users.up.fizz b/server/persistence/migrations/20231012102310_create_webauthn_users.up.fizz deleted file mode 100644 index bc49117..0000000 --- a/server/persistence/migrations/20231012102310_create_webauthn_users.up.fizz +++ /dev/null @@ -1,15 +0,0 @@ -create_table("webauthn_users") { - t.Column("id", "uuid", {primary: true}) - t.Column("user_id", "uuid", { unique: true }) - t.Column("name", "string", {}) - t.Column("icon", "string", {}) - t.Column("display_name", "string", {}) - - t.Timestamps() -} - -add_foreign_key("webauthn_credentials", "user_id", {"webauthn_users": ["id"]}, { - "name": "webauthn_user_credentials_fk", - "on_delete": "CASCADE", - "on_update": "CASCADE", -}) diff --git a/server/persistence/migrations/20231107190551_create_tenants.down.fizz b/server/persistence/migrations/20231107190551_create_tenants.down.fizz new file mode 100644 index 0000000..fb26891 --- /dev/null +++ b/server/persistence/migrations/20231107190551_create_tenants.down.fizz @@ -0,0 +1 @@ +drop_table("tenants") diff --git a/server/persistence/migrations/20231107190551_create_tenants.up.fizz b/server/persistence/migrations/20231107190551_create_tenants.up.fizz new file mode 100644 index 0000000..01af03d --- /dev/null +++ b/server/persistence/migrations/20231107190551_create_tenants.up.fizz @@ -0,0 +1,6 @@ +create_table("tenants") { + t.Column("id", "uuid", {primary: true}) + t.Column("display_name", "string", {}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107190727_create_configs.down.fizz b/server/persistence/migrations/20231107190727_create_configs.down.fizz new file mode 100644 index 0000000..b9387a0 --- /dev/null +++ b/server/persistence/migrations/20231107190727_create_configs.down.fizz @@ -0,0 +1 @@ +drop_table("configs") diff --git a/server/persistence/migrations/20231107190727_create_configs.up.fizz b/server/persistence/migrations/20231107190727_create_configs.up.fizz new file mode 100644 index 0000000..99a6fb2 --- /dev/null +++ b/server/persistence/migrations/20231107190727_create_configs.up.fizz @@ -0,0 +1,8 @@ +create_table("configs") { + t.Column("id", "uuid", {primary: true}) + t.Column("tenant_id", "uuid", { "null": false}) + + t.ForeignKey("tenant_id", {"tenants": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107190913_create_webauthn_configs.down.fizz b/server/persistence/migrations/20231107190913_create_webauthn_configs.down.fizz new file mode 100644 index 0000000..399638a --- /dev/null +++ b/server/persistence/migrations/20231107190913_create_webauthn_configs.down.fizz @@ -0,0 +1 @@ +drop_table("webauthn_configs") diff --git a/server/persistence/migrations/20231107190913_create_webauthn_configs.up.fizz b/server/persistence/migrations/20231107190913_create_webauthn_configs.up.fizz new file mode 100644 index 0000000..1417496 --- /dev/null +++ b/server/persistence/migrations/20231107190913_create_webauthn_configs.up.fizz @@ -0,0 +1,10 @@ +create_table("webauthn_configs") { + t.Column("id", "uuid", {primary: true}) + t.Column("timeout", "integer", { "null": false, default: 60000 }) + t.Column("user_verification", "string", { "null": false, default: "preferred"}) + t.Column("config_id", "uuid", { "null": false }) + + t.ForeignKey("config_id", {"configs": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107190959_create_relying_parties.down.fizz b/server/persistence/migrations/20231107190959_create_relying_parties.down.fizz new file mode 100644 index 0000000..9cedf2f --- /dev/null +++ b/server/persistence/migrations/20231107190959_create_relying_parties.down.fizz @@ -0,0 +1 @@ +drop_table("relying_parties") diff --git a/server/persistence/migrations/20231107190959_create_relying_parties.up.fizz b/server/persistence/migrations/20231107190959_create_relying_parties.up.fizz new file mode 100644 index 0000000..e01b1f7 --- /dev/null +++ b/server/persistence/migrations/20231107190959_create_relying_parties.up.fizz @@ -0,0 +1,11 @@ +create_table("relying_parties") { + t.Column("id", "uuid", {primary: true}) + t.Column("rp_id", "string", { "null": false }) + t.Column("display_name", "string", { "null": false}) + t.Column("icon", "string", {"null": true}) + t.Column("webauthn_config_id", "uuid", { "null": false, "unique": true }) + + t.ForeignKey("webauthn_config_id", {"webauthn_configs": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107191059_create_webauthn_origins.down.fizz b/server/persistence/migrations/20231107191059_create_webauthn_origins.down.fizz new file mode 100644 index 0000000..7b11d7a --- /dev/null +++ b/server/persistence/migrations/20231107191059_create_webauthn_origins.down.fizz @@ -0,0 +1 @@ +drop_table("webauthn_origins") diff --git a/server/persistence/migrations/20231107191059_create_webauthn_origins.up.fizz b/server/persistence/migrations/20231107191059_create_webauthn_origins.up.fizz new file mode 100644 index 0000000..128bc96 --- /dev/null +++ b/server/persistence/migrations/20231107191059_create_webauthn_origins.up.fizz @@ -0,0 +1,10 @@ +create_table("webauthn_origins") { + t.Column("id", "uuid", {primary: true}) + t.Column("origin", "string", { "null": false }) + t.Column("relying_party_id", "uuid", { "null": false }) + + t.Index(["origin", "relying_party_id"], { "unique": true }) + t.ForeignKey("relying_party_id", {"relying_parties": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107191208_create_cors.down.fizz b/server/persistence/migrations/20231107191208_create_cors.down.fizz new file mode 100644 index 0000000..7191992 --- /dev/null +++ b/server/persistence/migrations/20231107191208_create_cors.down.fizz @@ -0,0 +1 @@ +drop_table("cors") diff --git a/server/persistence/migrations/20231107191208_create_cors.up.fizz b/server/persistence/migrations/20231107191208_create_cors.up.fizz new file mode 100644 index 0000000..f93c398 --- /dev/null +++ b/server/persistence/migrations/20231107191208_create_cors.up.fizz @@ -0,0 +1,9 @@ +create_table("cors") { + t.Column("id", "uuid", {primary: true}) + t.Column("allow_unsafe", "boolean", {default: false}) + t.Column("config_id", "uuid", { "null": false }) + + t.ForeignKey("config_id", {"configs": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107191230_create_cors_origins.down.fizz b/server/persistence/migrations/20231107191230_create_cors_origins.down.fizz new file mode 100644 index 0000000..59dc859 --- /dev/null +++ b/server/persistence/migrations/20231107191230_create_cors_origins.down.fizz @@ -0,0 +1 @@ +drop_table("cors_origins") diff --git a/server/persistence/migrations/20231107191230_create_cors_origins.up.fizz b/server/persistence/migrations/20231107191230_create_cors_origins.up.fizz new file mode 100644 index 0000000..1ea1acb --- /dev/null +++ b/server/persistence/migrations/20231107191230_create_cors_origins.up.fizz @@ -0,0 +1,9 @@ +create_table("cors_origins") { + t.Column("id", "uuid", {primary: true}) + t.Column("origin", "string", { "null": false, "unique": true }) + t.Column("cors_id", "uuid", { "null": false}) + + t.ForeignKey("cors_id", {"cors": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107191346_create_audit_log_configs.down.fizz b/server/persistence/migrations/20231107191346_create_audit_log_configs.down.fizz new file mode 100644 index 0000000..63a5bc7 --- /dev/null +++ b/server/persistence/migrations/20231107191346_create_audit_log_configs.down.fizz @@ -0,0 +1 @@ +drop_table("audit_log_configs") diff --git a/server/persistence/migrations/20231107191346_create_audit_log_configs.up.fizz b/server/persistence/migrations/20231107191346_create_audit_log_configs.up.fizz new file mode 100644 index 0000000..224f10e --- /dev/null +++ b/server/persistence/migrations/20231107191346_create_audit_log_configs.up.fizz @@ -0,0 +1,11 @@ +create_table("audit_log_configs") { + t.Column("id", "uuid", {primary: true}) + t.Column("enable_storage", "boolean", { default: false}) + t.Column("enable_console", "boolean", { default: true}) + t.Column("output_stream", "string", { default: "stdout"}) + t.Column("config_id", "uuid", {"null": false}) + + t.ForeignKey("config_id", {"configs": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231107191413_create_secrets.down.fizz b/server/persistence/migrations/20231107191413_create_secrets.down.fizz new file mode 100644 index 0000000..eb95dff --- /dev/null +++ b/server/persistence/migrations/20231107191413_create_secrets.down.fizz @@ -0,0 +1 @@ +drop_table("secrets") diff --git a/server/persistence/migrations/20231107191413_create_secrets.up.fizz b/server/persistence/migrations/20231107191413_create_secrets.up.fizz new file mode 100644 index 0000000..2c85980 --- /dev/null +++ b/server/persistence/migrations/20231107191413_create_secrets.up.fizz @@ -0,0 +1,14 @@ +create_table("secrets") { + t.Column("id", "uuid", {primary: true}) + t.Column("name", "string", {}) + t.Column("key", "text", { "null": false }) + t.Column("is_api_secret", "boolean", { "default": true }) + t.Column("config_id", "uuid", { "null": false }) + + t.Index(["key", "config_id"], { "unique": true }) + t.ForeignKey("config_id", {"configs": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} + +add_index("secrets", ["name", "config_id"], {"unique": true}) diff --git a/server/persistence/migrations/20231107191655_create_webauthn_users.down.fizz b/server/persistence/migrations/20231107191655_create_webauthn_users.down.fizz new file mode 100644 index 0000000..3d27055 --- /dev/null +++ b/server/persistence/migrations/20231107191655_create_webauthn_users.down.fizz @@ -0,0 +1 @@ +drop_table("webauthn_users") diff --git a/server/persistence/migrations/20231107191655_create_webauthn_users.up.fizz b/server/persistence/migrations/20231107191655_create_webauthn_users.up.fizz new file mode 100644 index 0000000..57fde70 --- /dev/null +++ b/server/persistence/migrations/20231107191655_create_webauthn_users.up.fizz @@ -0,0 +1,13 @@ +create_table("webauthn_users") { + t.Column("id", "uuid", {primary: true}) + t.Column("user_id", "uuid", { unique:true }) + t.Column("name", "string", {}) + t.Column("icon", "string", {}) + t.Column("display_name", "string", {}) + t.Column("tenant_id", "uuid", { "null": false}) + + t.Index(["user_id", "tenant_id"], {"unique": true}) + t.ForeignKey("tenant_id", {"tenants": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + + t.Timestamps() +} diff --git a/server/persistence/migrations/20231011113417_webauthn_session_data.down.fizz b/server/persistence/migrations/20231107191924_create_webauthn_session_data.down.fizz similarity index 100% rename from server/persistence/migrations/20231011113417_webauthn_session_data.down.fizz rename to server/persistence/migrations/20231107191924_create_webauthn_session_data.down.fizz diff --git a/server/persistence/migrations/20231011113417_webauthn_session_data.up.fizz b/server/persistence/migrations/20231107191924_create_webauthn_session_data.up.fizz similarity index 70% rename from server/persistence/migrations/20231011113417_webauthn_session_data.up.fizz rename to server/persistence/migrations/20231107191924_create_webauthn_session_data.up.fizz index 80f7267..bbb4d1e 100644 --- a/server/persistence/migrations/20231011113417_webauthn_session_data.up.fizz +++ b/server/persistence/migrations/20231107191924_create_webauthn_session_data.up.fizz @@ -5,6 +5,11 @@ create_table("webauthn_session_data") { t.Column("user_verification", "string", {}) t.Column("operation", "string", {}) t.Column("expires_at", "timestamp", { "null": true }) + t.Column("tenant_id", "uuid", { "null": true }) + + t.ForeignKey("tenant_id", {"tenants": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + t.Timestamps() + t.Index("challenge", {"unique": true}) } diff --git a/server/persistence/migrations/20231011113424_webauthn_session_data_allowed_credential.down.fizz b/server/persistence/migrations/20231107192701_create_webauthn_session_data_allowed_creds.down.fizz similarity index 100% rename from server/persistence/migrations/20231011113424_webauthn_session_data_allowed_credential.down.fizz rename to server/persistence/migrations/20231107192701_create_webauthn_session_data_allowed_creds.down.fizz diff --git a/server/persistence/migrations/20231011113424_webauthn_session_data_allowed_credential.up.fizz b/server/persistence/migrations/20231107192701_create_webauthn_session_data_allowed_creds.up.fizz similarity index 99% rename from server/persistence/migrations/20231011113424_webauthn_session_data_allowed_credential.up.fizz rename to server/persistence/migrations/20231107192701_create_webauthn_session_data_allowed_creds.up.fizz index f1698ab..c040b30 100644 --- a/server/persistence/migrations/20231011113424_webauthn_session_data_allowed_credential.up.fizz +++ b/server/persistence/migrations/20231107192701_create_webauthn_session_data_allowed_creds.up.fizz @@ -2,6 +2,8 @@ create_table("webauthn_session_data_allowed_credentials") { t.Column("id", "uuid", {primary: true}) t.Column("credential_id", "string", {}) t.Column("webauthn_session_data_id", "uuid", {}) + t.Timestamps() + t.ForeignKey("webauthn_session_data_id", {"webauthn_session_data": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) } diff --git a/server/persistence/migrations/20231011113346_webauthn_credential.down.fizz b/server/persistence/migrations/20231107192813_create_webauthn_credentials.down.fizz similarity index 100% rename from server/persistence/migrations/20231011113346_webauthn_credential.down.fizz rename to server/persistence/migrations/20231107192813_create_webauthn_credentials.down.fizz diff --git a/server/persistence/migrations/20231011113346_webauthn_credential.up.fizz b/server/persistence/migrations/20231107192813_create_webauthn_credentials.up.fizz similarity index 75% rename from server/persistence/migrations/20231011113346_webauthn_credential.up.fizz rename to server/persistence/migrations/20231107192813_create_webauthn_credentials.up.fizz index 8ef73b9..42becb2 100644 --- a/server/persistence/migrations/20231011113346_webauthn_credential.up.fizz +++ b/server/persistence/migrations/20231107192813_create_webauthn_credentials.up.fizz @@ -9,5 +9,9 @@ create_table("webauthn_credentials") { t.Column("last_used_at", "timestamp", { "null": true }) t.Column("backup_eligible", "bool", { "default": false }) t.Column("backup_state", "bool", { "default": false }) + t.Column("webauthn_user_id", "uuid", { "null": true }) + + t.ForeignKey("webauthn_user_id", {"webauthn_users": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + t.Timestamps() } diff --git a/server/persistence/migrations/20231011113358_webauthn_credential_transport.down.fizz b/server/persistence/migrations/20231107192944_create_webauthn_credential_transports.down.fizz similarity index 100% rename from server/persistence/migrations/20231011113358_webauthn_credential_transport.down.fizz rename to server/persistence/migrations/20231107192944_create_webauthn_credential_transports.down.fizz diff --git a/server/persistence/migrations/20231011113358_webauthn_credential_transport.up.fizz b/server/persistence/migrations/20231107192944_create_webauthn_credential_transports.up.fizz similarity index 99% rename from server/persistence/migrations/20231011113358_webauthn_credential_transport.up.fizz rename to server/persistence/migrations/20231107192944_create_webauthn_credential_transports.up.fizz index 6bcc3f2..c0fd683 100644 --- a/server/persistence/migrations/20231011113358_webauthn_credential_transport.up.fizz +++ b/server/persistence/migrations/20231107192944_create_webauthn_credential_transports.up.fizz @@ -2,7 +2,10 @@ create_table("webauthn_credential_transports") { t.Column("id", "string", {"primary": true}) t.Column("name", "string", {}) t.Column("webauthn_credential_id", "string", {}) + t.DisableTimestamps() + t.ForeignKey("webauthn_credential_id", {"webauthn_credentials": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + t.Index(["name", "webauthn_credential_id"], {"unique": true}) } diff --git a/server/persistence/migrations/20231011113441_jwk.down.fizz b/server/persistence/migrations/20231107193018_create_jwks.down.fizz similarity index 100% rename from server/persistence/migrations/20231011113441_jwk.down.fizz rename to server/persistence/migrations/20231107193018_create_jwks.down.fizz diff --git a/server/persistence/migrations/20231011113441_jwk.up.fizz b/server/persistence/migrations/20231107193018_create_jwks.up.fizz similarity index 52% rename from server/persistence/migrations/20231011113441_jwk.up.fizz rename to server/persistence/migrations/20231107193018_create_jwks.up.fizz index 2110f9a..bf726ad 100644 --- a/server/persistence/migrations/20231011113441_jwk.up.fizz +++ b/server/persistence/migrations/20231107193018_create_jwks.up.fizz @@ -2,5 +2,9 @@ create_table("jwks") { t.Column("id", "int", {primary: true}) t.Column("key_data", "text", {}) t.Column("created_at", "timestamp", {}) + t.Column("tenant_id", "uuid", { "null": false }) + + t.ForeignKey("tenant_id", {"tenants": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + t.DisableTimestamps() } diff --git a/server/persistence/migrations/20231011113436_audit_log.down.fizz b/server/persistence/migrations/20231107193117_create_audit_logs.down.fizz similarity index 100% rename from server/persistence/migrations/20231011113436_audit_log.down.fizz rename to server/persistence/migrations/20231107193117_create_audit_logs.down.fizz diff --git a/server/persistence/migrations/20231011113436_audit_log.up.fizz b/server/persistence/migrations/20231107193117_create_audit_logs.up.fizz similarity index 75% rename from server/persistence/migrations/20231011113436_audit_log.up.fizz rename to server/persistence/migrations/20231107193117_create_audit_logs.up.fizz index 4c6ba5c..510ac84 100644 --- a/server/persistence/migrations/20231011113436_audit_log.up.fizz +++ b/server/persistence/migrations/20231107193117_create_audit_logs.up.fizz @@ -6,6 +6,10 @@ create_table("audit_logs") { t.Column("meta_source_ip", "string", {}) t.Column("meta_user_agent", "string", {}) t.Column("actor_user_id", "uuid", {"null": true}) + t.Column("tenant_id", "uuid", { "null": false }) + + t.ForeignKey("tenant_id", {"tenants": ["id"]}, {"on_delete": "cascade", "on_update": "cascade"}) + t.Timestamps() t.Index("type") t.Index("actor_user_id") diff --git a/server/persistence/models/audit_log.go b/server/persistence/models/audit_log.go index 6a14bb7..73675ea 100644 --- a/server/persistence/models/audit_log.go +++ b/server/persistence/models/audit_log.go @@ -16,8 +16,12 @@ type AuditLog struct { ActorUserId *uuid.UUID `db:"actor_user_id" json:"actor_user_id,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Tenant *Tenant `json:"tenant" belongs_to:"tenants"` + TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"` } +type AuditLogs []AuditLog + type AuditLogType string var ( diff --git a/server/persistence/models/audit_log_config.go b/server/persistence/models/audit_log_config.go new file mode 100644 index 0000000..eceb2ef --- /dev/null +++ b/server/persistence/models/audit_log_config.go @@ -0,0 +1,36 @@ +package models + +import ( + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// AuditLogConfig is used by pop to map your audit_log_configs database table to your go code. +type AuditLogConfig struct { + ID uuid.UUID `json:"id" db:"id"` + Config *Config `json:"config" belongs_to:"configs"` + ConfigID uuid.UUID `json:"config_id" db:"config_id"` + OutputStream string `json:"output_stream" db:"output_stream"` + ConsoleEnabled bool `json:"enable_console" db:"enable_console"` + StorageEnabled bool `json:"enable_storage" db:"enable_storage"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// AuditLogConfigs is not required by pop and may be deleted +type AuditLogConfigs []AuditLogConfig + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (auditLogConfig *AuditLogConfig) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: auditLogConfig.ID}, + &validators.StringIsPresent{Name: "OutputStream", Field: auditLogConfig.OutputStream}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: auditLogConfig.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: auditLogConfig.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/config.go b/server/persistence/models/config.go new file mode 100644 index 0000000..3a82b4d --- /dev/null +++ b/server/persistence/models/config.go @@ -0,0 +1,35 @@ +package models + +import ( + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gofrs/uuid" +) + +// Config is used by pop to map your tenant_configs database table to your go code. +type Config struct { + ID uuid.UUID `json:"id" db:"id"` + TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"` + Tenant *Tenant `json:"tenant,omitempty" belongs_to:"tenant"` + + WebauthnConfig WebauthnConfig `json:"webauthn_config,omitempty" has_one:"webauthn_config"` + Cors Cors `json:"cors,omitempty" has_one:"cor"` + AuditLogConfig AuditLogConfig `json:"audit_log_config,omitempty" has_one:"audit_log_config"` + Secrets Secrets `json:"secrets,omitempty" has_many:"secrets"` + + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (config *Config) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: config.ID}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: config.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: config.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/cors.go b/server/persistence/models/cors.go new file mode 100644 index 0000000..05cdc82 --- /dev/null +++ b/server/persistence/models/cors.go @@ -0,0 +1,31 @@ +package models + +import ( + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// Cors is used by pop to map your cors database table to your go code. +type Cors struct { + ID uuid.UUID `json:"id" db:"id"` + Config *Config `json:"config" belongs_to:"configs"` + ConfigID uuid.UUID `json:"config_id" db:"config_id"` + AllowUnsafe bool `json:"allow_unsafe" db:"allow_unsafe"` + Origins CorsOrigins `json:"origins" has_many:"cors_origins"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (cors *Cors) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: cors.ID}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: cors.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: cors.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/cors_origin.go b/server/persistence/models/cors_origin.go new file mode 100644 index 0000000..34739dc --- /dev/null +++ b/server/persistence/models/cors_origin.go @@ -0,0 +1,34 @@ +package models + +import ( + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// CorsOrigin is used by pop to map your cors_origins database table to your go code. +type CorsOrigin struct { + ID uuid.UUID `json:"id" db:"id"` + Cors *Cors `json:"cors" belongs_to:"cors"` + CorsID uuid.UUID `json:"cors_id" db:"cors_id"` + Origin string `json:"origin" db:"origin"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// CorsOrigins is not required by pop and may be deleted +type CorsOrigins []CorsOrigin + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (origin *CorsOrigin) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: origin.ID}, + &validators.StringIsPresent{Name: "Origin", Field: origin.Origin}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: origin.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: origin.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/jwk.go b/server/persistence/models/jwk.go index ebb4dea..5123afa 100644 --- a/server/persistence/models/jwk.go +++ b/server/persistence/models/jwk.go @@ -1,20 +1,27 @@ package models import ( - "github.com/gobuffalo/validate/v3/validators" + "github.com/gofrs/uuid" "time" + "github.com/gobuffalo/validate/v3/validators" + "github.com/gobuffalo/pop/v6" "github.com/gobuffalo/validate/v3" ) // Jwk is used by pop to map your jwks database table to your go code. type Jwk struct { - ID int `json:"id" db:"id"` - KeyData string `json:"key_data" db:"key_data"` + ID int `json:"id" db:"id"` + TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"` + Tenant *Tenant `json:"tenant" belongs_to:"tenants"` + KeyData string `json:"key_data" db:"key_data"` + CreatedAt time.Time `json:"created_at" db:"created_at"` } +type Jwks []Jwk + func (jwk *Jwk) Validate(tx *pop.Connection) (*validate.Errors, error) { return validate.Validate( &validators.StringIsPresent{Name: "KeyData", Field: jwk.KeyData}, diff --git a/server/persistence/models/relying_party.go b/server/persistence/models/relying_party.go new file mode 100644 index 0000000..341bfdf --- /dev/null +++ b/server/persistence/models/relying_party.go @@ -0,0 +1,38 @@ +package models + +import ( + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// RelyingParty is used by pop to map your relying_parties database table to your go code. +type RelyingParty struct { + ID uuid.UUID `json:"id" db:"id"` + WebauthnConfig *WebauthnConfig `json:"webauthn_config" belongs_to:"webauthn_configs"` + WebauthnConfigID uuid.UUID `json:"webauthn_config_id" db:"webauthn_config_id"` + RPId string `json:"rp_id" db:"rp_id"` + DisplayName string `json:"display_name" db:"display_name"` + Icon *string `json:"icon" db:"icon"` + Origins WebauthnOrigins `json:"origins" has_many:"webauthn_origins"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// RelyingParties is not required by pop and may be deleted +type RelyingParties []RelyingParty + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (rp *RelyingParty) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: rp.ID}, + &validators.StringIsPresent{Name: "RPId", Field: rp.RPId}, + &validators.StringIsPresent{Name: "DisplayName", Field: rp.DisplayName}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: rp.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: rp.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/secret.go b/server/persistence/models/secret.go new file mode 100644 index 0000000..040e4c1 --- /dev/null +++ b/server/persistence/models/secret.go @@ -0,0 +1,36 @@ +package models + +import ( + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gobuffalo/validate/v3/validators" + "github.com/gofrs/uuid" + "time" +) + +// Secret is used by pop to map your api_keys database table to your go code. +type Secret struct { + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Key string `json:"key" db:"key"` + IsAPISecret bool `json:"is_api_secret" db:"is_api_secret"` + ConfigID uuid.UUID `json:"config_id" db:"config_id"` + Config *Config `json:"config" belongs_to:"configs"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// Secrets is not required by pop and may be deleted +type Secrets []Secret + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (secret *Secret) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: secret.ID}, + &validators.StringIsPresent{Name: "Name", Field: secret.Name}, + &validators.StringIsPresent{Name: "Key", Field: secret.Key}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: secret.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: secret.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/tenant.go b/server/persistence/models/tenant.go new file mode 100644 index 0000000..3d6fa44 --- /dev/null +++ b/server/persistence/models/tenant.go @@ -0,0 +1,39 @@ +package models + +import ( + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// Tenant is used by pop to map your tenants database table to your go code. +type Tenant struct { + ID uuid.UUID `json:"id" db:"id"` + DisplayName string `json:"display_name" db:"display_name"` + + Config Config `json:"config" has_one:"config"` + AuditLogs AuditLogs `json:"audit_logs,omitempty" has_many:"audit_logs"` + Jwks Jwks `json:"jwks,omitempty" has_many:"jwks"` + SessionData []WebauthnSessionData `has_many:"webauthn_session_data"` + WebauthnUsers WebauthnUsers `json:"webauthn_users,omitempty" has_many:"webauthn_users"` + + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// Tenants is not required by pop and may be deleted +type Tenants []Tenant + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (tenant *Tenant) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: tenant.ID}, + &validators.StringIsPresent{Name: "DisplayName", Field: tenant.DisplayName}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: tenant.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: tenant.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/webauthn_config.go b/server/persistence/models/webauthn_config.go new file mode 100644 index 0000000..3a71b7b --- /dev/null +++ b/server/persistence/models/webauthn_config.go @@ -0,0 +1,35 @@ +package models + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// WebauthnConfig is used by pop to map your webauthn_configs database table to your go code. +type WebauthnConfig struct { + ID uuid.UUID `json:"id" db:"id"` + Config *Config `json:"config" belongs_to:"configs"` + ConfigID uuid.UUID `json:"config_id" db:"config_id"` + RelyingParty RelyingParty `json:"relying_party" has_one:"relying_parties"` + Timeout int `json:"timeout" db:"timeout"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + UserVerification protocol.UserVerificationRequirement `json:"user_verification" db:"user_verification"` +} + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (webauthn *WebauthnConfig) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: webauthn.ID}, + &validators.IntIsPresent{Name: "Timeout", Field: webauthn.Timeout}, + &validators.StringIsPresent{Name: "UserVerification", Field: string(webauthn.UserVerification)}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: webauthn.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: webauthn.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/webauthn_credential.go b/server/persistence/models/webauthn_credential.go index b9f49d2..1591ac5 100644 --- a/server/persistence/models/webauthn_credential.go +++ b/server/persistence/models/webauthn_credential.go @@ -12,8 +12,8 @@ import ( // WebauthnCredential is used by pop to map your webauthn_credentials database table to your go code. type WebauthnCredential struct { ID string `db:"id" json:"id"` - Name *string `db:"name" json:"-"` UserId uuid.UUID `db:"user_id" json:"-"` + Name *string `db:"name" json:"-"` PublicKey string `db:"public_key" json:"-"` AttestationType string `db:"attestation_type" json:"-"` AAGUID uuid.UUID `db:"aaguid" json:"-"` @@ -24,8 +24,13 @@ type WebauthnCredential struct { Transports Transports `has_many:"webauthn_credential_transports" json:"-"` BackupEligible bool `db:"backup_eligible" json:"-"` BackupState bool `db:"backup_state" json:"-"` + + WebauthnUserID uuid.UUID `db:"webauthn_user_id"` + WebauthnUser *WebauthnUser `belongs_to:"webauthn_user"` } +type WebauthnCredentials []WebauthnCredential + // Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. func (credential *WebauthnCredential) Validate(tx *pop.Connection) (*validate.Errors, error) { return validate.Validate( diff --git a/server/persistence/models/webauthn_origin.go b/server/persistence/models/webauthn_origin.go new file mode 100644 index 0000000..13d3246 --- /dev/null +++ b/server/persistence/models/webauthn_origin.go @@ -0,0 +1,34 @@ +package models + +import ( + "github.com/gobuffalo/validate/v3/validators" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gobuffalo/validate/v3" + "github.com/gofrs/uuid" +) + +// WebauthnOrigin is used by pop to map your webauthn_origins database table to your go code. +type WebauthnOrigin struct { + ID uuid.UUID `json:"id" db:"id"` + RelyingParty *RelyingParty `json:"relying_party" belongs_to:"relying_parties"` + RelyingPartyID uuid.UUID `json:"relying_party_id" db:"relying_party_id"` + Origin string `json:"origin" db:"origin"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// WebauthnOrigins is not required by pop and may be deleted +type WebauthnOrigins []WebauthnOrigin + +// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. +// This method is not required and may be deleted. +func (origin *WebauthnOrigin) Validate(tx *pop.Connection) (*validate.Errors, error) { + return validate.Validate( + &validators.UUIDIsPresent{Name: "ID", Field: origin.ID}, + &validators.StringIsPresent{Name: "Origin", Field: origin.Origin}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: origin.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: origin.CreatedAt}, + ), nil +} diff --git a/server/persistence/models/webauthn_session_data.go b/server/persistence/models/webauthn_session_data.go index 3de739b..e80fb0d 100644 --- a/server/persistence/models/webauthn_session_data.go +++ b/server/persistence/models/webauthn_session_data.go @@ -20,14 +20,17 @@ var ( // WebauthnSessionData is used by pop to map your webauthn_session_data database table to your go code. type WebauthnSessionData struct { ID uuid.UUID `db:"id"` + UserId uuid.UUID `db:"user_id" json:"-"` Challenge string `db:"challenge"` - UserId uuid.UUID `db:"user_id"` UserVerification string `db:"user_verification"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` Operation Operation `db:"operation"` AllowedCredentials []WebauthnSessionDataAllowedCredential `has_many:"webauthn_session_data_allowed_credentials"` ExpiresAt nulls.Time `db:"expires_at"` + + TenantID uuid.UUID `db:"tenant_id"` + Tenant *Tenant `belongs_to:"tenants"` } // Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. diff --git a/server/persistence/models/webauthn_user.go b/server/persistence/models/webauthn_user.go index 61c4537..009b217 100644 --- a/server/persistence/models/webauthn_user.go +++ b/server/persistence/models/webauthn_user.go @@ -13,42 +13,47 @@ import ( // WebauthnUser is used by pop to map your webauthn_users database table to your go code. type WebauthnUser struct { - ID uuid.UUID `json:"id" db:"id"` - UserID uuid.UUID `json:"user_id" db:"user_id"` - Name string `json:"name" db:"name"` - Icon string `json:"icon" db:"icon"` - DisplayName string `json:"display_name" db:"display_name"` - Credentials []WebauthnCredential `has_many:"webauthn_credentials"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + ID uuid.UUID `json:"id" db:"id"` + UserID uuid.UUID `json:"user_id" db:"user_id"` + Name string `json:"name" db:"name"` + Icon string `json:"icon" db:"icon"` + DisplayName string `json:"display_name" db:"display_name"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Tenant *Tenant `json:"tenant" belongs_to:"tenant"` + TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"` + + WebauthnCredentials WebauthnCredentials `json:"webauthn_credentials,omitempty" has_many:"webauthn_credentials"` } -func (u *WebauthnUser) WebAuthnID() []byte { - return u.UserID.Bytes() +type WebauthnUsers []WebauthnUser + +func (webauthnUser *WebauthnUser) WebAuthnID() []byte { + return webauthnUser.UserID.Bytes() } -func (u *WebauthnUser) WebAuthnName() string { - return u.Name +func (webauthnUser *WebauthnUser) WebAuthnName() string { + return webauthnUser.Name } -func (u *WebauthnUser) WebAuthnDisplayName() string { - return u.DisplayName +func (webauthnUser *WebauthnUser) WebAuthnDisplayName() string { + return webauthnUser.DisplayName } -func (u *WebauthnUser) WebAuthnIcon() string { - return u.Icon +func (webauthnUser *WebauthnUser) WebAuthnIcon() string { + return webauthnUser.Icon } // Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. // This method is not required and may be deleted. -func (w *WebauthnUser) Validate(tx *pop.Connection) (*validate.Errors, error) { +func (webauthnUser *WebauthnUser) Validate(tx *pop.Connection) (*validate.Errors, error) { return validate.Validate( - &validators.UUIDIsPresent{Name: "ID", Field: w.ID}, - &validators.UUIDIsPresent{Name: "UserID", Field: w.UserID}, - &validators.StringIsPresent{Name: "Name", Field: w.Name}, - &validators.StringIsPresent{Name: "DisplayName", Field: w.DisplayName}, - &validators.TimeIsPresent{Name: "UpdatedAt", Field: w.UpdatedAt}, - &validators.TimeIsPresent{Name: "CreatedAt", Field: w.CreatedAt}, + &validators.UUIDIsPresent{Name: "ID", Field: webauthnUser.ID}, + &validators.UUIDIsPresent{Name: "UserID", Field: webauthnUser.UserID}, + &validators.StringIsPresent{Name: "Name", Field: webauthnUser.Name}, + &validators.StringIsPresent{Name: "DisplayName", Field: webauthnUser.DisplayName}, + &validators.TimeIsPresent{Name: "UpdatedAt", Field: webauthnUser.UpdatedAt}, + &validators.TimeIsPresent{Name: "CreatedAt", Field: webauthnUser.CreatedAt}, ), nil } diff --git a/server/persistence/persister.go b/server/persistence/persister.go index e4dbb41..26a14b5 100644 --- a/server/persistence/persister.go +++ b/server/persistence/persister.go @@ -24,6 +24,13 @@ type Persister interface { GetWebauthnSessionDataPersister(tx *pop.Connection) persisters.WebauthnSessionDataPersister GetWebauthnUserPersister(tx *pop.Connection) persisters.WebauthnUserPersister GetJwkPersister(tx *pop.Connection) persisters.JwkPersister + GetTenantPersister(tx *pop.Connection) persisters.TenantPersister + GetSecretsPersister(tx *pop.Connection) persisters.SecretsPersister + GetConfigPersister(tx *pop.Connection) persisters.ConfigPersister + GetWebauthnConfigPersister(tx *pop.Connection) persisters.WebauthnConfigPersister + GetWebauthnRelyingPartyPersister(tx *pop.Connection) persisters.WebauthnRelyingPartyPersister + GetCorsPersister(tx *pop.Connection) persisters.CorsPersister + GetAuditLogConfigPersister(tx *pop.Connection) persisters.AuditLogConfigPersister } type Migrator interface { @@ -141,3 +148,59 @@ func (p *persister) GetJwkPersister(tx *pop.Connection) persisters.JwkPersister return persisters.NewJwkPersister(tx) } + +func (p *persister) GetTenantPersister(tx *pop.Connection) persisters.TenantPersister { + if tx == nil { + return persisters.NewTenantPersister(p.Database) + } + + return persisters.NewTenantPersister(tx) +} + +func (p *persister) GetSecretsPersister(tx *pop.Connection) persisters.SecretsPersister { + if tx == nil { + return persisters.NewSecretsPersister(p.Database) + } + + return persisters.NewSecretsPersister(tx) +} + +func (p *persister) GetConfigPersister(tx *pop.Connection) persisters.ConfigPersister { + if tx == nil { + return persisters.NewConfigPersister(p.Database) + } + + return persisters.NewConfigPersister(tx) +} + +func (p *persister) GetWebauthnConfigPersister(tx *pop.Connection) persisters.WebauthnConfigPersister { + if tx == nil { + return persisters.NewWebauthnConfigPersister(p.Database) + } + + return persisters.NewWebauthnConfigPersister(tx) +} + +func (p *persister) GetWebauthnRelyingPartyPersister(tx *pop.Connection) persisters.WebauthnRelyingPartyPersister { + if tx == nil { + return persisters.NewWebauthnRelyingPartyPersister(p.Database) + } + + return persisters.NewWebauthnRelyingPartyPersister(tx) +} + +func (p *persister) GetCorsPersister(tx *pop.Connection) persisters.CorsPersister { + if tx == nil { + return persisters.NewCorsPersister(p.Database) + } + + return persisters.NewCorsPersister(tx) +} + +func (p *persister) GetAuditLogConfigPersister(tx *pop.Connection) persisters.AuditLogConfigPersister { + if tx == nil { + return persisters.NewAuditLogConfigPersister(p.Database) + } + + return persisters.NewAuditLogConfigPersister(tx) +} diff --git a/server/persistence/persisters/audit_log_config_persister.go b/server/persistence/persisters/audit_log_config_persister.go new file mode 100644 index 0000000..7a7a5a3 --- /dev/null +++ b/server/persistence/persisters/audit_log_config_persister.go @@ -0,0 +1,32 @@ +package persisters + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type AuditLogConfigPersister interface { + Create(auditLogConfig *models.AuditLogConfig) error +} + +type auditLogConfigPersister struct { + database *pop.Connection +} + +func NewAuditLogConfigPersister(database *pop.Connection) AuditLogConfigPersister { + return &auditLogConfigPersister{database: database} +} + +func (ap *auditLogConfigPersister) Create(auditLogConfig *models.AuditLogConfig) error { + validationErr, err := ap.database.Eager().ValidateAndCreate(auditLogConfig) + if err != nil { + return fmt.Errorf("failed to store audit log config: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("audit log config validation failed: %w", validationErr) + } + + return nil +} diff --git a/server/persistence/persisters/audit_log_persister.go b/server/persistence/persisters/audit_log_persister.go index c598c3f..8249f9a 100644 --- a/server/persistence/persisters/audit_log_persister.go +++ b/server/persistence/persisters/audit_log_persister.go @@ -67,7 +67,7 @@ type AuditLogOptions struct { } func (p *auditLogPersister) List(options AuditLogOptions) ([]models.AuditLog, error) { - auditLogs := []models.AuditLog{} + var auditLogs []models.AuditLog query := p.database.Q() query = p.addQueryParamsToSqlQuery(query, options) diff --git a/server/persistence/persisters/config_persister.go b/server/persistence/persisters/config_persister.go new file mode 100644 index 0000000..c94a6ee --- /dev/null +++ b/server/persistence/persisters/config_persister.go @@ -0,0 +1,44 @@ +package persisters + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type ConfigPersister interface { + Create(config *models.Config) error + Delete(config *models.Config) error +} + +type configPersister struct { + database *pop.Connection +} + +func NewConfigPersister(database *pop.Connection) ConfigPersister { + return &configPersister{ + database: database, + } +} + +func (cp *configPersister) Create(config *models.Config) error { + validationErr, err := cp.database.ValidateAndCreate(config) + if err != nil { + return fmt.Errorf("failed to store config: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("config validation failed: %w", validationErr) + } + + return nil +} + +func (cp *configPersister) Delete(config *models.Config) error { + err := cp.database.Eager().Destroy(config) + if err != nil { + return fmt.Errorf("failed to delete config: %w", err) + } + + return nil +} diff --git a/server/persistence/persisters/cors_persister.go b/server/persistence/persisters/cors_persister.go new file mode 100644 index 0000000..dd4daca --- /dev/null +++ b/server/persistence/persisters/cors_persister.go @@ -0,0 +1,32 @@ +package persisters + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type CorsPersister interface { + Create(cors *models.Cors) error +} + +type corsPersister struct { + database *pop.Connection +} + +func NewCorsPersister(database *pop.Connection) CorsPersister { + return &corsPersister{database: database} +} + +func (cp *corsPersister) Create(cors *models.Cors) error { + validationErr, err := cp.database.Eager().ValidateAndCreate(cors) + if err != nil { + return fmt.Errorf("failed to store cors: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("cors validation failed: %w", validationErr) + } + + return nil +} diff --git a/server/persistence/persisters/jwk_persister.go b/server/persistence/persisters/jwk_persister.go index 4706078..4143ba7 100644 --- a/server/persistence/persisters/jwk_persister.go +++ b/server/persistence/persisters/jwk_persister.go @@ -4,17 +4,25 @@ import ( "database/sql" "errors" "fmt" + "github.com/gofrs/uuid" + "github.com/gobuffalo/pop/v6" "github.com/teamhanko/passkey-server/persistence/models" ) type JwkPersister interface { Get(int) (*models.Jwk, error) + GetByKeyAndTenantId(keyData string, tenantId uuid.UUID) (*models.Jwk, error) GetAll() ([]models.Jwk, error) - GetLast() (*models.Jwk, error) + GetAllForTenant(tenantId uuid.UUID) ([]models.Jwk, error) + GetLast(tenantId uuid.UUID) (*models.Jwk, error) Create(models.Jwk) error } +const ( + GetFailureMessageFormat = "failed to get jwk: %w" +) + type jwkPersister struct { db *pop.Connection } @@ -30,13 +38,25 @@ func (p *jwkPersister) Get(id int) (*models.Jwk, error) { return nil, nil } if err != nil { - return nil, fmt.Errorf("failed to get jwk: %w", err) + return nil, fmt.Errorf(GetFailureMessageFormat, err) + } + return &jwk, nil +} + +func (p *jwkPersister) GetByKeyAndTenantId(keyData string, tenantId uuid.UUID) (*models.Jwk, error) { + jwk := models.Jwk{} + err := p.db.Eager().Where("key_data = ? AND tenant_id = ?", &keyData, &tenantId).First(&jwk) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf(GetFailureMessageFormat, err) } return &jwk, nil } func (p *jwkPersister) GetAll() ([]models.Jwk, error) { - jwks := []models.Jwk{} + var jwks []models.Jwk err := p.db.All(&jwks) if err != nil && errors.Is(err, sql.ErrNoRows) { return nil, nil @@ -47,14 +67,26 @@ func (p *jwkPersister) GetAll() ([]models.Jwk, error) { return jwks, nil } -func (p *jwkPersister) GetLast() (*models.Jwk, error) { +func (p *jwkPersister) GetAllForTenant(tenantId uuid.UUID) ([]models.Jwk, error) { + var jwks []models.Jwk + err := p.db.Where("tenant_id = ?", &tenantId).All(&jwks) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get all jwks: %w", err) + } + return jwks, nil +} + +func (p *jwkPersister) GetLast(tenantId uuid.UUID) (*models.Jwk, error) { jwk := models.Jwk{} - err := p.db.Last(&jwk) + err := p.db.Where("tenant_id = ?", tenantId).Last(&jwk) if err != nil && errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { - return nil, fmt.Errorf("failed to get jwk: %w", err) + return nil, fmt.Errorf(GetFailureMessageFormat, err) } return &jwk, nil } diff --git a/server/persistence/persisters/secrets_persister.go b/server/persistence/persisters/secrets_persister.go new file mode 100644 index 0000000..1a0de13 --- /dev/null +++ b/server/persistence/persisters/secrets_persister.go @@ -0,0 +1,56 @@ +package persisters + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type SecretsPersister interface { + Create(secret *models.Secret) error + Delete(secret *models.Secret) error + Update(secret *models.Secret) error +} + +type secretsPersister struct { + database *pop.Connection +} + +func NewSecretsPersister(database *pop.Connection) SecretsPersister { + return &secretsPersister{database: database} +} + +func (sp secretsPersister) Create(secret *models.Secret) error { + validationErr, err := sp.database.ValidateAndCreate(secret) + if err != nil { + return fmt.Errorf("failed to store secret: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("secret validation failed: %w", validationErr) + } + + return nil +} + +func (sp secretsPersister) Delete(secret *models.Secret) error { + err := sp.database.Eager().Destroy(secret) + if err != nil { + return fmt.Errorf("failed to delete secret: %w", err) + } + + return nil +} + +func (sp secretsPersister) Update(secret *models.Secret) error { + validationErr, err := sp.database.ValidateAndUpdate(secret) + if err != nil { + return fmt.Errorf("failed to update secret: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("secret validation failed: %w", validationErr) + } + + return nil +} diff --git a/server/persistence/persisters/tenant_persister.go b/server/persistence/persisters/tenant_persister.go new file mode 100644 index 0000000..c5f5a97 --- /dev/null +++ b/server/persistence/persisters/tenant_persister.go @@ -0,0 +1,97 @@ +package persisters + +import ( + "database/sql" + "errors" + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type TenantPersister interface { + Create(tenant *models.Tenant) error + Get(tenantId uuid.UUID) (*models.Tenant, error) + List() (models.Tenants, error) + Update(tenant *models.Tenant) error + Delete(tenant *models.Tenant) error +} + +type tenantPersister struct { + database *pop.Connection +} + +func NewTenantPersister(database *pop.Connection) TenantPersister { + return &tenantPersister{database: database} +} + +func (t tenantPersister) Create(tenant *models.Tenant) error { + validationErr, err := t.database.ValidateAndCreate(tenant) + if err != nil { + return fmt.Errorf("failed to store tenant: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("tenant validation failed: %w", validationErr) + } + + return nil +} + +func (t tenantPersister) Get(tenantId uuid.UUID) (*models.Tenant, error) { + tenant := models.Tenant{} + err := t.database.Eager( + "Config.Secrets", + "Config.WebauthnConfig.RelyingParty.Origins", + "Config.Cors.Origins", + "Config.AuditLogConfig", + ).Find(&tenant, tenantId) + + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + + if err != nil { + fmt.Printf("failed to get Tenant: %v\n", err) + return nil, fmt.Errorf("failed to get tenant: %w", err) + } + + return &tenant, nil +} + +func (t tenantPersister) List() (models.Tenants, error) { + tenants := models.Tenants{} + err := t.database.All(&tenants) + + if err != nil && errors.Is(err, sql.ErrNoRows) { + return tenants, nil + } + + if err != nil { + return nil, fmt.Errorf("failed to get tenants: %w", err) + } + + return tenants, nil +} + +func (t tenantPersister) Update(tenant *models.Tenant) error { + validationErr, err := t.database.ValidateAndUpdate(tenant) + if err != nil { + return fmt.Errorf("failed to store tenant: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("tenant validation failed: %w", validationErr) + } + + return nil +} + +func (t tenantPersister) Delete(tenant *models.Tenant) error { + err := t.database.Destroy(tenant) + if err != nil { + return fmt.Errorf("failed to delete tenant: %w", err) + } + + return nil +} diff --git a/server/persistence/persisters/webauthn_config_persister.go b/server/persistence/persisters/webauthn_config_persister.go new file mode 100644 index 0000000..f11bd2f --- /dev/null +++ b/server/persistence/persisters/webauthn_config_persister.go @@ -0,0 +1,32 @@ +package persisters + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type WebauthnConfigPersister interface { + Create(webauthnConfigModel *models.WebauthnConfig) error +} + +type webauthnConfigPersister struct { + database *pop.Connection +} + +func NewWebauthnConfigPersister(database *pop.Connection) WebauthnConfigPersister { + return &webauthnConfigPersister{database: database} +} + +func (wp *webauthnConfigPersister) Create(webauthnConfigModel *models.WebauthnConfig) error { + validationErr, err := wp.database.ValidateAndCreate(webauthnConfigModel) + if err != nil { + return fmt.Errorf("failed to store webauthnConfigModel: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("webauthnConfigModel validation failed: %w", validationErr) + } + + return nil +} diff --git a/server/persistence/persisters/webauthn_credential_persister.go b/server/persistence/persisters/webauthn_credential_persister.go index 782f8e3..d64504d 100644 --- a/server/persistence/persisters/webauthn_credential_persister.go +++ b/server/persistence/persisters/webauthn_credential_persister.go @@ -42,7 +42,7 @@ func (w *webauthnCredentialPersister) Get(id string) (*models.WebauthnCredential } func (w *webauthnCredentialPersister) Create(credential *models.WebauthnCredential) error { - vErr, err := w.database.ValidateAndCreate(&credential) + vErr, err := w.database.ValidateAndCreate(credential) if err != nil { return fmt.Errorf("failed to store credential: %w", err) } @@ -53,7 +53,7 @@ func (w *webauthnCredentialPersister) Create(credential *models.WebauthnCredenti // Eager creation seems to be broken, so we need to store the transports separately. // See: https://github.com/gobuffalo/pop/issues/608 - vErr, err = w.database.ValidateAndCreate(&credential.Transports) + vErr, err = w.database.ValidateAndCreate(credential.Transports) if err != nil { return fmt.Errorf("failed to store credential transport: %w", err) } diff --git a/server/persistence/persisters/webauthn_relying_party_persister.go b/server/persistence/persisters/webauthn_relying_party_persister.go new file mode 100644 index 0000000..8b20157 --- /dev/null +++ b/server/persistence/persisters/webauthn_relying_party_persister.go @@ -0,0 +1,32 @@ +package persisters + +import ( + "fmt" + "github.com/gobuffalo/pop/v6" + "github.com/teamhanko/passkey-server/persistence/models" +) + +type WebauthnRelyingPartyPersister interface { + Create(relyingParty *models.RelyingParty) error +} + +type webauthnRelyingPartyPersister struct { + database *pop.Connection +} + +func NewWebauthnRelyingPartyPersister(database *pop.Connection) WebauthnRelyingPartyPersister { + return &webauthnRelyingPartyPersister{database: database} +} + +func (wp *webauthnRelyingPartyPersister) Create(relyingParty *models.RelyingParty) error { + validationErr, err := wp.database.Eager().ValidateAndCreate(relyingParty) + if err != nil { + return fmt.Errorf("failed to store relying party: %w", err) + } + + if validationErr != nil && validationErr.HasAny() { + return fmt.Errorf("relying party validation failed: %w", validationErr) + } + + return nil +} diff --git a/server/persistence/persisters/webauthn_sessiondata_persister.go b/server/persistence/persisters/webauthn_sessiondata_persister.go index bf12804..faca640 100644 --- a/server/persistence/persisters/webauthn_sessiondata_persister.go +++ b/server/persistence/persisters/webauthn_sessiondata_persister.go @@ -12,7 +12,7 @@ import ( type WebauthnSessionDataPersister interface { Get(id uuid.UUID) (*models.WebauthnSessionData, error) - GetByChallenge(challenge string) (*models.WebauthnSessionData, error) + GetByChallenge(challenge string, tenantId uuid.UUID) (*models.WebauthnSessionData, error) Create(sessionData models.WebauthnSessionData) error Update(sessionData models.WebauthnSessionData) error Delete(sessionData models.WebauthnSessionData) error @@ -39,9 +39,9 @@ func (w *webauthnSessionDataPersister) Get(id uuid.UUID) (*models.WebauthnSessio return &sessionData, nil } -func (w *webauthnSessionDataPersister) GetByChallenge(challenge string) (*models.WebauthnSessionData, error) { +func (w *webauthnSessionDataPersister) GetByChallenge(challenge string, tenantId uuid.UUID) (*models.WebauthnSessionData, error) { var sessionData []models.WebauthnSessionData - err := w.database.Eager().Where("challenge = ?", challenge).All(&sessionData) + err := w.database.Eager().Where("challenge = ? AND tenant_id = ?", challenge, tenantId).All(&sessionData) if err != nil && errors.Is(err, sql.ErrNoRows) { return nil, nil } diff --git a/server/persistence/persisters/webauthn_user_persister.go b/server/persistence/persisters/webauthn_user_persister.go index f361059..6955ab6 100644 --- a/server/persistence/persisters/webauthn_user_persister.go +++ b/server/persistence/persisters/webauthn_user_persister.go @@ -13,7 +13,7 @@ import ( type WebauthnUserPersister interface { Create(webauthnUser *models.WebauthnUser) error Get(id uuid.UUID) (*models.WebauthnUser, error) - GetByUserId(userId uuid.UUID) (*models.WebauthnUser, error) + GetByUserId(userId uuid.UUID, tenantId uuid.UUID) (*models.WebauthnUser, error) Delete(webauthnUser *models.WebauthnUser) error } @@ -31,18 +31,14 @@ func (p *webauthnUserPersister) Create(webauthnUser *models.WebauthnUser) error vErr, err := p.database.ValidateAndCreate(webauthnUser) if err != nil { fmt.Printf("%s", err.Error()) - fmt.Println("Here5") return fmt.Errorf("failed to store webauthn user: %w", err) } if vErr != nil && vErr.HasAny() { - fmt.Println("Here6") fmt.Printf("%s", vErr.Error()) fmt.Printf("Debug: %v", webauthnUser) return fmt.Errorf("webauthn user object validation failed: %w", vErr) } - fmt.Println("here7") - return nil } @@ -68,9 +64,9 @@ func (p *webauthnUserPersister) Delete(webauthnUser *models.WebauthnUser) error return nil } -func (p *webauthnUserPersister) GetByUserId(userId uuid.UUID) (*models.WebauthnUser, error) { +func (p *webauthnUserPersister) GetByUserId(userId uuid.UUID, tenantId uuid.UUID) (*models.WebauthnUser, error) { weauthnUser := models.WebauthnUser{} - err := p.database.Where("user_id = ?", userId).First(&weauthnUser) + err := p.database.Eager().Where("user_id = ? AND tenant_id = ?", userId, tenantId).First(&weauthnUser) if err != nil && errors.Is(err, sql.ErrNoRows) { return nil, nil } diff --git a/spec/passkey-server-admin.yaml b/spec/passkey-server-admin.yaml new file mode 100644 index 0000000..23c6d49 --- /dev/null +++ b/spec/passkey-server-admin.yaml @@ -0,0 +1,508 @@ +openapi: 3.1.0 +info: + version: '1.0' + title: passkey-server-admin + summary: Admin API for Passkey Server + description: 'ADmin API for Hanko Passkey Server. Allows creation and configiration of tenants and api keys, ' + termsOfService: 'https://www.hanko.io/terms' + contact: + name: Developers@hanko.io + url: 'https://hanko.io' + email: developers@hanko.io + license: + url: 'https://www.gnu.org/licenses/gpl-3.0.de.html' + name: GPLv3 +servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' +paths: + /tenants: + get: + summary: Get tenant list + description: Get a list of all Tenants + operationId: get-admin-tenant + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/tenant_list' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + post: + summary: Create a tenant + description: Create a new tenant + operationId: post-admin-tenant + requestBody: + $ref: '#/components/requestBodies/create_tenant' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/tenant_api_key' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + '/tenants/{tenant_id}': + get: + summary: Get a tenant + description: Get detailed information about the tenant + operationId: get-admin-tenant-tenant_id + parameters: + - $ref: '#/components/parameters/tenant_id' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/tenant' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + put: + summary: Update tenant + description: Update information of a tenant + operationId: put-admin-tenant-tenant_id + parameters: + - $ref: '#/components/parameters/tenant_id' + requestBody: + $ref: '#/components/requestBodies/update_tenant' + responses: + '204': + description: No Content + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + delete: + summary: Delete tenant + description: Remove a tenant + operationId: delete-admin-tenant-tenant_id + parameters: + - $ref: '#/components/parameters/tenant_id' + responses: + '204': + description: No Content + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + '/tenants/{tenant_id}/secrets/jwk': + post: + summary: Create secret + description: Creates a new JWT encryption key + operationId: post-admin-tenant-tenant_id-secrets-jwk + parameters: + - $ref: '#/components/parameters/tenant_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + required: + - name + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/secret' + description: The created secret + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + get: + summary: '' + description: Get all JWKs as list + operationId: get-path_prefix-tenants-tenant_id-secrets-jwk + parameters: + - $ref: '#/components/parameters/tenant_id' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/secret_list' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + '/tenants/{tenant_id}/secrets/api': + post: + summary: Create API Key + description: Creates a new API key + operationId: post-admin-tenant-tenant_id-secrets-api + parameters: + - $ref: '#/components/parameters/tenant_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + required: + - name + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/secret' + description: The created secret + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + get: + summary: List API keys + description: Get all API keys as list + operationId: get-path_prefix-tenants-tenant_id-secrets-api + parameters: + - $ref: '#/components/parameters/tenant_id' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/secret_list' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + '/tenants/{tenant_id}/secrets/jwk/{secret_id}': + delete: + summary: Remove JWK + description: Remove a JWK + operationId: delete-admin-tenant-tenant_id-secrets-jwk-secret_id + parameters: + - $ref: '#/components/parameters/tenant_id' + - $ref: '#/components/parameters/secret_id' + responses: + '204': + description: No Content + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + '/tenants/{tenant_id}/secrets/api/{secret_id}': + delete: + summary: Remove API key + description: Remove an api key + operationId: delete-admin-tenant-tenant_id-secrets-api-secret-id + parameters: + - $ref: '#/components/parameters/tenant_id' + - $ref: '#/components/parameters/secret_id' + responses: + '204': + description: No Content + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' + '/tenants/{tenant_id}/config': + put: + summary: Update config + description: Update config + operationId: put-admin-tenant-tenant_id-config + parameters: + - $ref: '#/components/parameters/tenant_id' + requestBody: + $ref: '#/components/requestBodies/update_config' + responses: + '204': + description: No Content + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + description: Host-Part of the URL + default: localhost + path_prefix: + description: Path-Prefix + default: '' +tags: + - name: admin api + description: Hanko Passkey-Server Admin API +components: + parameters: + tenant_id: + name: tenant_id + in: path + description: UUID of the tenant + required: true + schema: + type: string + format: uuid + minLength: 36 + maxLength: 36 + examples: + - 1f496bcd-49da-4839-a02f-7ce681ccb488 + secret_id: + name: secret_id + in: path + description: UUID of a secret + required: true + schema: + type: string + format: uuid + minLength: 36 + maxLength: 36 + requestBodies: + create_tenant: + content: + application/json: + schema: + type: object + properties: + display_name: + type: string + config: + $ref: '#/components/schemas/config' + required: + - display_name + update_tenant: + content: + application/json: + schema: + type: object + properties: + display_name: + type: string + required: + - display_name + update_config: + content: + application/json: + schema: + $ref: '#/components/schemas/config' + schemas: + tenant_list: + type: object + title: tenant_list + properties: + id: + type: string + format: uuid + maxLength: 36 + display_name: + type: string + required: + - id + - display_name + tenant_api_key: + type: object + title: tenant_api_key + properties: + id: + type: string + format: uuid + minLength: 36 + maxLength: 36 + api_key: + $ref: '#/components/schemas/secret' + required: + - id + - api_key + secret: + type: object + title: secret + description: Entiy Model of a secret + properties: + id: + type: string + format: uuid + minLength: 36 + maxLength: 36 + name: + type: string + secret: + type: string + minLength: 36 + created_at: + type: string + format: date-time + required: + - id + - name + - secret + - created_at + tenant: + title: tenant + allOf: + - type: object + additionalProperties: false + properties: + config: + $ref: '#/components/schemas/config' + required: + - config + minProperties: 3 + maxProperties: 3 + - $ref: '#/components/schemas/tenant_list' + config: + type: object + title: config + properties: + cors: + $ref: '#/components/schemas/cors' + webauthn: + $ref: '#/components/schemas/webauthn' + required: + - cors + - webauthn + cors: + type: object + title: cors + properties: + allowed_origins: + type: array + minItems: 1 + items: + type: string + examples: + - '*.example.local' + allow_unsafe_wildcard: + type: boolean + default: false + required: + - allowed_origins + - allow_unsafe_wildcard + webauthn: + type: object + title: webauthn + properties: + relying_party: + $ref: '#/components/schemas/relying_party' + timeout: + type: number + default: 60000 + examples: + - 60000 + user_verification: + enum: + - required + - preferred + - discouraged + required: + - relying_party + - timeout + - user_verification + relying_party: + type: object + title: relying_party + description: Relying Party part of config + properties: + id: + type: string + default: localhost + examples: + - localhost + display_name: + type: string + default: Hanko Passkey Server + examples: + - Hanko Passkey Server + icon: + type: + - string + - 'null' + format: uri + examples: + - 'http://link.to/icon' + origins: + type: array + minItems: 1 + uniqueItems: true + items: + type: string + required: + - id + - display_name + secret_list: + type: array + title: secret_list + uniqueItems: true + items: + $ref: '#/components/schemas/secret' diff --git a/spec/passkey-server.yaml b/spec/passkey-server.yaml index ccd224d..8752ae9 100644 --- a/spec/passkey-server.yaml +++ b/spec/passkey-server.yaml @@ -8,14 +8,21 @@ info: contact: email: developers@hanko.io url: hanko.io - name: Developers@hanko + name: Developers@hanko.io license: url: 'https://www.gnu.org/licenses/gpl-3.0.de.html' name: GPLv3 servers: - - url: 'http://localhost:8000' + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' paths: - /credentials: + '/{tenant_id}/credentials': get: tags: - credentials @@ -25,6 +32,7 @@ paths: parameters: - $ref: '#/components/parameters/X-API-KEY' - $ref: '#/components/parameters/user_id' + - $ref: '#/components/parameters/tenant_id' requestBody: description: '' content: {} @@ -39,8 +47,15 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' - '/credentials/{credential_id}': + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' + '/{tenant_id}/credentials/{credential_id}': patch: tags: - credentials @@ -50,6 +65,7 @@ paths: parameters: - $ref: '#/components/parameters/X-API-KEY' - $ref: '#/components/parameters/credential_id' + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/patch-credential' responses: @@ -65,13 +81,21 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' delete: summary: Remove Credential description: Endpoint for removing a webauthn credential operationId: delete-credentials-credentialId parameters: - $ref: '#/components/parameters/credential_id' + - $ref: '#/components/parameters/tenant_id' responses: '204': description: No Content @@ -85,8 +109,15 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' - /registration/initialize: + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' + '/{tenant_id}/registration/initialize': post: tags: - credentials @@ -95,6 +126,7 @@ paths: operationId: post-registration-initialize parameters: - $ref: '#/components/parameters/X-API-KEY' + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-registration-initialize' responses: @@ -108,14 +140,23 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' - /registration/finalize: + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' + '/{tenant_id}/registration/finalize': post: tags: - credentials summary: Finish Passkey Registration description: Finish credential registration process. operationId: post-registration-finalize + parameters: + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-registration-finalize' responses: @@ -131,12 +172,21 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' - /login/initialize: + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' + '/{tenant_id}/login/initialize': post: summary: Start Login description: Initialize a login flow for passkeys operationId: post-login-initialize + parameters: + - $ref: '#/components/parameters/tenant_id' responses: '200': $ref: '#/components/responses/post-login-initialize' @@ -150,12 +200,21 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' - /login/finalize: + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' + '/{tenant_id}/login/finalize': post: summary: Finish Login description: Finalize the login operation operationId: post-login-finalize + parameters: + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-login-finalize' responses: @@ -171,8 +230,15 @@ paths: $ref: '#/components/responses/error' security: [] servers: - - url: 'http://localhost:8000' - /.well-known/jwks.json: + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' + '/{tenant_id}/.well-known/jwks.json': get: tags: - credentials @@ -180,11 +246,20 @@ paths: summary: Well-known JWKS description: Endpoint for fetching JWKS operationId: get-.well-known-jwks.json + parameters: + - $ref: '#/components/parameters/tenant_id' responses: '200': $ref: '#/components/responses/jwks' servers: - - url: 'http://localhost:8000' + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + description: Host Part of the URL + default: localhost + path_prefix: + description: Path prefix + default: '' tags: - name: credentials description: Represents all objects which are related to webauthn credentials @@ -216,6 +291,18 @@ components: maxLength: 36 examples: - 1f496bcd-49da-4839-a02f-7ce681cdaaaa + tenant_id: + name: tenant_id + in: path + description: UUID of the tenant + required: true + schema: + type: string + format: uuid + minLength: 36 + maxLength: 36 + examples: + - 1f496bcd-49da-4839-a02f-7ce681ccb488 X-API-KEY: name: apiKey in: header @@ -382,41 +469,47 @@ components: schema: type: object properties: - rp: - $ref: '#/components/schemas/relying-party-entity' - user: - $ref: '#/components/schemas/user-entity' - challenge: - type: string - pubKeyCredParams: - type: - - array - - 'null' - uniqueItems: true - items: - $ref: '#/components/schemas/credential-parameter-entity' - timeout: - type: - - integer - - 'null' - excludeCredentials: - type: - - array - - 'null' - items: - $ref: '#/components/schemas/credential-descriptor-entity' - authenticatorSelection: - $ref: '#/components/schemas/authentication-selection-entity' - attestation: - type: string - extensions: - type: array - items: - type: object + publicKey: + type: object + additionalProperties: false + properties: + rp: + $ref: '#/components/schemas/relying-party-entity' + user: + $ref: '#/components/schemas/user-entity' + challenge: + type: string + pubKeyCredParams: + type: + - array + - 'null' + items: + $ref: '#/components/schemas/credential-parameter-entity' + timeout: + type: + - integer + - 'null' + excludeCredentials: + type: + - array + - 'null' + items: + $ref: '#/components/schemas/credential-descriptor-entity' + authenticatorSelection: + $ref: '#/components/schemas/authentication-selection-entity' + attestation: + type: string + extensions: + type: array + uniqueItems: true + items: + type: object + required: + - rp + - user + - challenge required: - - rp - - user - - challenge + - publicKey post-login-initialize: description: Example response content: