Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat extend user import #1992

Merged
merged 17 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 88 additions & 22 deletions backend/cmd/user/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package user
import (
"errors"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofrs/uuid"
"github.com/invopop/jsonschema"
"time"

"github.com/gofrs/uuid"
)

// ImportOrExportEmail The import/export format for a user's email
type ImportOrExportEmail struct {
// Address Valid email address
Address string `json:"address" yaml:"address" jsonschema:"format=email"`
Address string `json:"address" yaml:"address" jsonschema:"format=email" validate:"email"`
// IsPrimary indicates if this is the primary email of the users. In the Emails array there has to be exactly one primary email.
IsPrimary bool `json:"is_primary" yaml:"is_primary"`
// IsVerified indicates if the email address was previously verified.
Expand All @@ -26,16 +26,76 @@ func (ImportOrExportEmail) JSONSchemaExtend(schema *jsonschema.Schema) {
// Emails Array of email addresses
type Emails []ImportOrExportEmail

type ImportWebauthnCredential struct {
// ID of the WebAuthn credential.
ID string `json:"id" yaml:"id" validate:"required"`
// Optional Name of the WebAuthn credential.
Name *string `json:"name" yaml:"name" validate:"omitempty"`
// The PublicKey of the credential.
PublicKey string `json:"public_key" yaml:"public_key" validate:"required"`
// The AttestationType the credential was created with.
AttestationType string `json:"attestation_type" yaml:"attestation_type" validate:"required"`
// Optional AAGUID of the authenticator on which the credential was created on.
AAGUID uuid.UUID `json:"aaguid" yaml:"aaguid" validate:"omitempty,uuid4"`
// Optional SignCount of the WebAuthn credential.
SignCount int `json:"sign_count" yaml:"sign_count"`
// LastUsedAt optional timestamp when the WebAuthn credential was last used.
LastUsedAt *time.Time `json:"last_used_at" yaml:"last_used_at" validate:"omitempty"`
// CreatedAt optional timestamp of the WebAuthn credentials' creation. Will be set to the import date if not provided.
CreatedAt *time.Time `json:"created_at" yaml:"created_at" validate:"omitempty"`
// UpdatedAt optional timestamp of the last update to the WebAuthn credential. Will be set to the import date if not provided.
UpdatedAt *time.Time `json:"updated_at" yaml:"updated_at" validate:"omitempty"`
// Optional list of supported Transports by the authenticator.
Transports []string `json:"transports" yaml:"transports" validate:"omitempty,unique"`
// BackupEligible flag indicates if the WebAuthn credential can be backed up (e.g. in Apple KeyChain, ...). If the information is not available set it to false.
BackupEligible bool `json:"backup_eligible" yaml:"backup_eligible"`
// BackupState flag indicates if the WebAuthn credential is backed up (e.g. in Apple KeyChain, ...). If the information is not available set it to false.
BackupState bool `json:"backup_state" yaml:"backup_state"`
// MFAOnly flag indicates if the WebAuthn credential can only be used in combination with another login factor (e.g. password, ...).
MFAOnly bool `json:"mfa_only" yaml:"mfa_only"`
// UserHandle optional user id which was used to create the credential with.
// Populate only when user id was not an uuid v4 and the WebAuthn credential is not an MFAOnly credential.
UserHandle *string `json:"user_handle" yaml:"user_handle" validate:"omitempty,excluded_if=MFAOnly true"`
}

type ImportWebauthnCredentials []ImportWebauthnCredential

type ImportPasswordCredential struct {
// Password hash of the password in bcrypt format.
Password string `json:"password" yaml:"password" validate:"required,startswith=$2a$"`
// CreatedAt optional timestamp when the password was created. Will be set to the import date if not provided.
CreatedAt *time.Time `json:"created_at,omitempty" yaml:"created_at" validate:"omitempty"`
// UpdatedAt optional timestamp of the last update to the password. Will be set to the import date if not provided.
UpdatedAt *time.Time `json:"updated_at,omitempty" yaml:"updated_at" validate:"omitempty"`
}

type ImportOTPSecret struct {
// Secret of the TOTP credential. TOTP credential must be generated for a period of 30 seconds and SHA1 hash algorithm.
Secret string `json:"secret" yaml:"secret" validate:"required"`
// CreatedAt optional timestamp when the otp secret was created. Will be set to the import date if not provided.
CreatedAt *time.Time `json:"created_at,omitempty" yaml:"created_at" validate:"omitempty"`
// UpdatedAt optional timestamp of the last update to the otp secret. Will be set to the import date if not provided.
UpdatedAt *time.Time `json:"updated_at,omitempty" yaml:"updated_at" validate:"omitempty"`
}

// ImportOrExportEntry represents a user to be imported/export to the Hanko database
type ImportOrExportEntry struct {
// UserID optional uuid.v4. If not provided a new one will be generated for the user
UserID string `json:"user_id,omitempty" yaml:"user_id"`
// Emails List of emails
Emails Emails `json:"emails" yaml:"emails" jsonschema:"type=array,minItems=1"`
UserID string `json:"user_id,omitempty" yaml:"user_id" validate:"omitempty,uuid4"`
// Emails optional list of emails
Emails Emails `json:"emails" yaml:"emails" jsonschema:"type=array,minItems=1" validate:"required_if=Username 0,unique=Address,dive"`
// Username optional username of the user
Username *string `json:"username,omitempty" yaml:"username" validate:"required_if=Emails 0,omitempty,gte=1"`
// WebauthnCredentials optional list of WebAuthn credentials of a user. Includes passkeys and MFA credentials.
WebauthnCredentials ImportWebauthnCredentials `json:"webauthn_credentials,omitempty" yaml:"webauthn_credentials" validate:"omitempty,unique=ID,dive"`
// Password optional password.
Password *ImportPasswordCredential `json:"password" yaml:"password" validate:"omitempty"`
// OTPSecret optional TOTP secret for MFA.
OTPSecret *ImportOTPSecret `json:"otp_secret" yaml:"otp_secret" validate:"omitempty"`
// CreatedAt optional timestamp of the users' creation. Will be set to the import date if not provided.
CreatedAt *time.Time `json:"created_at,omitempty" yaml:"created_at"`
CreatedAt *time.Time `json:"created_at,omitempty" yaml:"created_at" validate:"omitempty"`
// UpdatedAt optional timestamp of the last update to the user. Will be set to the import date if not provided.
UpdatedAt *time.Time `json:"updated_at,omitempty" yaml:"updated_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty" yaml:"updated_at" validate:"omitempty"`
}

func (ImportOrExportEntry) JSONSchemaExtend(schema *jsonschema.Schema) {
Expand All @@ -47,6 +107,7 @@ type ImportOrExportList []ImportOrExportEntry

func (ImportOrExportList) JSONSchemaExtend(schema *jsonschema.Schema) {
date := time.Date(2024, 8, 17, 12, 5, 15, 651387237, time.UTC)
username := "example"
schema.Examples = []any{
[]ImportOrExportEntry{
{
Expand All @@ -68,29 +129,34 @@ func (ImportOrExportList) JSONSchemaExtend(schema *jsonschema.Schema) {
UpdatedAt: &date,
},
},
[]ImportOrExportEntry{
{
Username: &username,
Password: &ImportPasswordCredential{
Password: "$2a$12$mFbud0mLsD/q.WG7/9pNQemlAHs3H4o8zAv44gsUF1v1awsdqTh7.",
CreatedAt: &date,
UpdatedAt: &date,
},
},
},
}
}

func (entry *ImportOrExportEntry) validate() error {
if len(entry.Emails) == 0 {
return errors.New(fmt.Sprintf("Entry with id: %v has got no Emails.", entry.UserID))
func (entry *ImportOrExportEntry) validate(v *validator.Validate) error {
err := v.Struct(entry)
if err != nil {
return err
}
primaryMails := 0
primaryEmailAddresses := 0
for _, email := range entry.Emails {
//TODO: Validate email
if email.IsPrimary {
primaryMails++
primaryEmailAddresses++
}
}

if primaryMails != 1 {
return errors.New(fmt.Sprintf("Need exactly one primary email, got %v", primaryMails))
}
if entry.UserID != "" {
_, err := uuid.FromString(entry.UserID)
if err != nil {
return errors.New(fmt.Sprintf("Provided uuid is not valid: %v", entry.UserID))
}
if len(entry.Emails) > 0 && primaryEmailAddresses != 1 {
return errors.New(fmt.Sprintf("Need exactly one primary email, got %v", primaryEmailAddresses))
}

return nil
}
Loading
Loading