Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
758 lines
23 KiB
Go
758 lines
23 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package identity
|
|
|
|
import (
|
|
"context"
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/pkg/errors"
|
|
"github.com/samber/lo"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/kratos/cipher"
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/x/pagination/keysetpagination"
|
|
"github.com/ory/x/region"
|
|
"github.com/ory/x/sqlxx"
|
|
)
|
|
|
|
// An Identity's State
|
|
//
|
|
// The state can either be `active` or `inactive`.
|
|
//
|
|
// swagger:enum State
|
|
type State string
|
|
|
|
const (
|
|
StateActive State = "active"
|
|
StateInactive State = "inactive"
|
|
)
|
|
|
|
func (lt State) IsValid() error {
|
|
switch lt {
|
|
case StateActive, StateInactive:
|
|
return nil
|
|
}
|
|
return errors.New("identity state is not valid")
|
|
}
|
|
|
|
// Identity represents an Ory Kratos identity
|
|
//
|
|
// An [identity](https://www.ory.com/docs/kratos/concepts/identity-user-model) represents a (human) user in Ory.
|
|
//
|
|
// swagger:model identity
|
|
type Identity struct {
|
|
// ID is the identity's unique identifier.
|
|
//
|
|
// The Identity ID can not be changed and can not be chosen. This ensures future
|
|
// compatibility and optimization for distributed stores such as CockroachDB.
|
|
//
|
|
// required: true
|
|
ID uuid.UUID `json:"id" faker:"-" db:"id"`
|
|
|
|
// ExternalID is an optional external ID of the identity. This is used to link
|
|
// the identity to an external system. If set, the external ID must be unique
|
|
// across all identities.
|
|
//
|
|
// required: false
|
|
ExternalID sqlxx.NullString `json:"external_id,omitempty" faker:"-" db:"external_id"`
|
|
|
|
// Credentials represents all credentials that can be used for authenticating this identity.
|
|
Credentials map[CredentialsType]Credentials `json:"credentials,omitempty" faker:"-" db:"-"`
|
|
|
|
// InternalAvailableAAL defines the maximum available AAL for this identity.
|
|
//
|
|
// - If the user has at least one two-factor authentication method configured, the AAL will be 2.
|
|
// - If the user has only a password configured, the AAL will be 1.
|
|
//
|
|
// This field is AAL2 as soon as a second factor credential is found. A first factor is not required for this
|
|
// field to return `aal2`.
|
|
//
|
|
// This field is primarily used to determine whether the user needs to upgrade to AAL2 without having to check
|
|
// all the credentials in the database. Use with caution!
|
|
InternalAvailableAAL NullableAuthenticatorAssuranceLevel `json:"-" faker:"-" db:"available_aal"`
|
|
|
|
// // IdentifierCredentials contains the access and refresh token for oidc identifier
|
|
// IdentifierCredentials []IdentifierCredential `json:"identifier_credentials,omitempty" faker:"-" db:"-"`
|
|
|
|
// SchemaID is the ID of the JSON Schema to be used for validating the identity's traits.
|
|
//
|
|
// required: true
|
|
SchemaID string `json:"schema_id" faker:"-" db:"schema_id"`
|
|
|
|
// SchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.
|
|
//
|
|
// format: url
|
|
// required: true
|
|
SchemaURL string `json:"schema_url" faker:"-" db:"-"`
|
|
|
|
// State is the identity's state.
|
|
//
|
|
// This value has currently no effect.
|
|
State State `json:"state" faker:"-" db:"state"`
|
|
|
|
// StateChangedAt contains the last time when the identity's state changed.
|
|
StateChangedAt *sqlxx.NullTime `json:"state_changed_at,omitempty" faker:"-" db:"state_changed_at"`
|
|
|
|
// Traits represent an identity's traits. The identity is able to create, modify, and delete traits
|
|
// in a self-service manner. The input will always be validated against the JSON Schema defined
|
|
// in `schema_url`.
|
|
//
|
|
// required: true
|
|
Traits Traits `json:"traits" faker:"-" db:"traits"`
|
|
|
|
// VerifiableAddresses contains all the addresses that can be verified by the user.
|
|
//
|
|
// Extensions:
|
|
// ---
|
|
// x-omitempty: true
|
|
// ---
|
|
VerifiableAddresses []VerifiableAddress `json:"verifiable_addresses,omitempty" faker:"-" has_many:"identity_verifiable_addresses" fk_id:"identity_id" order_by:"id asc"`
|
|
|
|
// RecoveryAddresses contains all the addresses that can be used to recover an identity.
|
|
//
|
|
// Extensions:
|
|
// ---
|
|
// x-omitempty: true
|
|
// ---
|
|
RecoveryAddresses []RecoveryAddress `json:"recovery_addresses,omitempty" faker:"-" has_many:"identity_recovery_addresses" fk_id:"identity_id" order_by:"id asc"`
|
|
|
|
// Store metadata about the identity which the identity itself can see when calling for example the
|
|
// session endpoint. Do not store sensitive information (e.g. credit score) about the identity in this field.
|
|
MetadataPublic sqlxx.NullJSONRawMessage `json:"metadata_public" faker:"-" db:"metadata_public"`
|
|
|
|
// Store metadata about the user which is only accessible through admin APIs such as `GET /admin/identities/<id>`.
|
|
MetadataAdmin sqlxx.NullJSONRawMessage `json:"metadata_admin,omitempty" faker:"-" db:"metadata_admin"`
|
|
|
|
// CreatedAt is a helper struct field for gobuffalo.pop.
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
|
|
// UpdatedAt is a helper struct field for gobuffalo.pop.
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
NID uuid.UUID `json:"-" faker:"-" db:"nid"`
|
|
OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"`
|
|
|
|
// Region is the Ory Network region this identity is homed in. Set by
|
|
// the multi-region persister; empty on OSS and single-region deployments.
|
|
Region region.Region `json:"region,omitempty" db:"-"`
|
|
}
|
|
|
|
func (i *Identity) PageToken() keysetpagination.PageToken {
|
|
return keysetpagination.StringPageToken(i.ID.String())
|
|
}
|
|
|
|
func DefaultPageToken() keysetpagination.PageToken {
|
|
return keysetpagination.StringPageToken(uuid.Nil.String())
|
|
}
|
|
|
|
// Traits represent an identity's traits. The identity is able to create, modify, and delete traits
|
|
// in a self-service manner. The input will always be validated against the JSON Schema defined
|
|
// in `schema_url`.
|
|
//
|
|
// swagger:model identityTraits
|
|
type Traits json.RawMessage
|
|
|
|
func (t *Traits) Scan(value any) error {
|
|
return sqlxx.JSONScan(t, value)
|
|
}
|
|
|
|
func (t Traits) Value() (driver.Value, error) {
|
|
return string(t), nil
|
|
}
|
|
|
|
func (t *Traits) String() string {
|
|
return string(*t)
|
|
}
|
|
|
|
// MarshalJSON returns m as the JSON encoding of m.
|
|
func (t Traits) MarshalJSON() ([]byte, error) {
|
|
if t == nil {
|
|
return []byte("null"), nil
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// UnmarshalJSON sets *m to a copy of data.
|
|
func (t *Traits) UnmarshalJSON(data []byte) error {
|
|
if t == nil {
|
|
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
|
}
|
|
*t = append((*t)[0:0], data...)
|
|
return nil
|
|
}
|
|
|
|
func (i Identity) TableName(context.Context) string {
|
|
return "identities"
|
|
}
|
|
|
|
func (i *Identity) IsActive() bool {
|
|
return i.State == StateActive
|
|
}
|
|
|
|
func (i *Identity) SetCredentials(t CredentialsType, c Credentials) {
|
|
if i.Credentials == nil {
|
|
i.Credentials = make(map[CredentialsType]Credentials)
|
|
}
|
|
|
|
c.Type = t
|
|
c.IdentityID = i.ID
|
|
i.Credentials[t] = c
|
|
}
|
|
|
|
func (i *Identity) SetCredentialsWithConfig(t CredentialsType, c Credentials, conf any) (err error) {
|
|
if i.Credentials == nil {
|
|
i.Credentials = make(map[CredentialsType]Credentials)
|
|
}
|
|
|
|
c.Config, err = json.Marshal(conf)
|
|
if err != nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode %s credentials to JSON: %s", t, err))
|
|
}
|
|
|
|
c.Type = t
|
|
i.Credentials[t] = c
|
|
return nil
|
|
}
|
|
|
|
func (i *Identity) DeleteCredentialsType(t CredentialsType) {
|
|
if i.Credentials == nil {
|
|
return
|
|
}
|
|
|
|
delete(i.Credentials, t)
|
|
}
|
|
|
|
// GetCredentialsOr returns the credentials for a given CredentialsType. If the
|
|
// credentials do not exist, the fallback is returned.
|
|
func (i *Identity) GetCredentialsOr(t CredentialsType, fallback *Credentials) *Credentials {
|
|
c, ok := i.GetCredentials(t)
|
|
if !ok {
|
|
return fallback
|
|
}
|
|
return c
|
|
}
|
|
|
|
type CredentialsOptions func(c *Credentials)
|
|
|
|
func WithAdditionalIdentifier(identifier string) CredentialsOptions {
|
|
return func(c *Credentials) {
|
|
c.Identifiers = append(c.Identifiers, identifier)
|
|
}
|
|
}
|
|
|
|
func (i *Identity) UpsertCredentialsConfig(t CredentialsType, conf []byte, version int, opt ...CredentialsOptions) {
|
|
c, ok := i.GetCredentials(t)
|
|
if !ok {
|
|
c = &Credentials{}
|
|
}
|
|
|
|
for _, optionFn := range opt {
|
|
optionFn(c)
|
|
}
|
|
|
|
c.Type = t
|
|
c.IdentityID = i.ID
|
|
c.Config = conf
|
|
c.Version = version
|
|
|
|
i.SetCredentials(t, *c)
|
|
}
|
|
|
|
func (i *Identity) GetCredentials(t CredentialsType) (*Credentials, bool) {
|
|
if c, ok := i.Credentials[t]; ok {
|
|
return &c, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func (i *Identity) ParseCredentials(t CredentialsType, config any) (*Credentials, error) {
|
|
if c, ok := i.Credentials[t]; ok {
|
|
if err := json.Unmarshal(c.Config, config); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
return nil, herodot.ErrNotFound().WithReasonf("identity does not have credential type %s", t)
|
|
}
|
|
|
|
func (i *Identity) CopyWithoutCredentials() *Identity {
|
|
ii := *i
|
|
ii.Credentials = nil
|
|
return &ii
|
|
}
|
|
|
|
// MergeOIDCCredentials merges the new credentials into the existing credentials.
|
|
// If the provider already exists, the provider is replace and the identifier is
|
|
// updated. This function requires the new credentials to have exactly one
|
|
// provider and one identifier, as returned by identity.NewCredentialsOIDC.
|
|
func (i *Identity) MergeOIDCCredentials(t CredentialsType, newCreds Credentials) (err error) {
|
|
creds, ok := i.Credentials[t]
|
|
if !ok {
|
|
i.SetCredentials(t, newCreds)
|
|
return nil
|
|
}
|
|
|
|
var conf CredentialsOIDC
|
|
if err = json.Unmarshal(creds.Config, &conf); err != nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode old %s credentials from JSON: %s", creds.Config, err))
|
|
}
|
|
|
|
var newConf CredentialsOIDC
|
|
if err = json.Unmarshal(newCreds.Config, &newConf); err != nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode new %s credentials from JSON: %s", newCreds.Config, err))
|
|
}
|
|
|
|
if len(newConf.Providers) != 1 {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Expected exactly one provider to merge credentials."))
|
|
}
|
|
newProvider := newConf.Providers[0]
|
|
|
|
// The identifier should have been set already, but we set it here just in case.
|
|
if len(newCreds.Identifiers) != 1 {
|
|
newCreds.Identifiers = []string{OIDCUniqueID(newProvider.Provider, newProvider.Subject)}
|
|
}
|
|
|
|
// Delete `use_auto_link` providers and their identifiers
|
|
var obsoleteIdentifiers []string
|
|
conf.Providers = slices.DeleteFunc(conf.Providers, func(p CredentialsOIDCProvider) bool {
|
|
if p.Provider == newProvider.Provider && p.UseAutoLink {
|
|
obsoleteIdentifiers = append(obsoleteIdentifiers, OIDCUniqueID(p.Provider, p.Subject))
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
creds.Identifiers = slices.DeleteFunc(creds.Identifiers, func(identifier string) bool {
|
|
return slices.Contains(obsoleteIdentifiers, identifier)
|
|
})
|
|
|
|
creds.Identifiers = append(creds.Identifiers, newCreds.Identifiers...)
|
|
conf.Providers = append(conf.Providers, newProvider)
|
|
|
|
return i.SetCredentialsWithConfig(t, creds, conf)
|
|
}
|
|
|
|
func NewIdentity(traitsSchemaID string) *Identity {
|
|
if traitsSchemaID == "" {
|
|
traitsSchemaID = config.DefaultIdentityTraitsSchemaID
|
|
}
|
|
|
|
stateChangedAt := sqlxx.NullTime(time.Now().UTC())
|
|
return &Identity{
|
|
ID: uuid.Nil,
|
|
Credentials: map[CredentialsType]Credentials{},
|
|
Traits: Traits("{}"),
|
|
SchemaID: traitsSchemaID,
|
|
VerifiableAddresses: []VerifiableAddress{},
|
|
State: StateActive,
|
|
StateChangedAt: &stateChangedAt,
|
|
}
|
|
}
|
|
|
|
func (i Identity) GetID() uuid.UUID {
|
|
return i.ID
|
|
}
|
|
|
|
func (i Identity) MarshalJSON() ([]byte, error) {
|
|
type localIdentity Identity
|
|
i.Credentials = nil
|
|
i.MetadataAdmin = nil
|
|
result, err := json.Marshal(localIdentity(i))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (i *Identity) UnmarshalJSON(b []byte) error {
|
|
type localIdentity Identity
|
|
err := json.Unmarshal(b, (*localIdentity)(i))
|
|
i.Credentials = nil
|
|
i.MetadataAdmin = nil
|
|
return err
|
|
}
|
|
|
|
// SetAvailableAAL sets the InternalAvailableAAL field based on the credentials stored in the identity.
|
|
//
|
|
// If a second factor is set up, the AAL will be set to 2. If only a first factor is set up, the AAL will be set to 1.
|
|
//
|
|
// A first factor is NOT required for the AAL to be set to 2 if a second factor is set up.
|
|
func (i *Identity) SetAvailableAAL(ctx context.Context, m *Manager) (err error) {
|
|
if c, err := m.CountActiveMultiFactorCredentials(ctx, i); err != nil {
|
|
return err
|
|
} else if c > 0 {
|
|
i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel2)
|
|
return nil
|
|
}
|
|
|
|
if c, err := m.CountActiveFirstFactorCredentials(ctx, i); err != nil {
|
|
return err
|
|
} else if c > 0 {
|
|
i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel1)
|
|
return nil
|
|
}
|
|
|
|
i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(NoAuthenticatorAssuranceLevel)
|
|
return nil
|
|
}
|
|
|
|
type WithAdminMetadataInJSON Identity
|
|
|
|
func (i WithAdminMetadataInJSON) MarshalJSON() ([]byte, error) {
|
|
type localIdentity Identity
|
|
i.Credentials = nil
|
|
return json.Marshal(localIdentity(i))
|
|
}
|
|
|
|
type WithCredentialsAndAdminMetadataInJSON Identity
|
|
|
|
func (i WithCredentialsAndAdminMetadataInJSON) MarshalJSON() ([]byte, error) {
|
|
type localIdentity Identity
|
|
return json.Marshal(localIdentity(i))
|
|
}
|
|
|
|
type WithCredentialsNoConfigAndAdminMetadataInJSON Identity
|
|
|
|
func (i WithCredentialsNoConfigAndAdminMetadataInJSON) MarshalJSON() ([]byte, error) {
|
|
type localIdentity Identity
|
|
for k, v := range i.Credentials {
|
|
v.Config = nil
|
|
i.Credentials[k] = v
|
|
}
|
|
return json.Marshal(localIdentity(i))
|
|
}
|
|
|
|
func (i *Identity) Validate() error {
|
|
expected := i.NID
|
|
if expected == uuid.Nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReason("Received empty nid."))
|
|
}
|
|
|
|
i.RecoveryAddresses = lo.Filter(i.RecoveryAddresses, func(v RecoveryAddress, key int) bool {
|
|
return v.NID == expected
|
|
})
|
|
|
|
i.VerifiableAddresses = lo.Filter(i.VerifiableAddresses, func(v VerifiableAddress, key int) bool {
|
|
return v.NID == expected
|
|
})
|
|
|
|
for k := range i.Credentials {
|
|
c := i.Credentials[k]
|
|
if c.NID != expected {
|
|
delete(i.Credentials, k)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CollectVerifiableAddresses returns a slice of all verifiable addresses of the given identities.
|
|
func CollectVerifiableAddresses(i []*Identity) (res []VerifiableAddress) {
|
|
res = make([]VerifiableAddress, 0, len(i))
|
|
for _, id := range i {
|
|
res = append(res, id.VerifiableAddresses...)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// CollectRecoveryAddresses returns a slice of all recovery addresses of the given identities.
|
|
func CollectRecoveryAddresses(i []*Identity) (res []RecoveryAddress) {
|
|
res = make([]RecoveryAddress, 0, len(i))
|
|
for _, id := range i {
|
|
res = append(res, id.RecoveryAddresses...)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Provider, includeCredentials []CredentialsType) (*Identity, error) {
|
|
credsToPublish := make(map[CredentialsType]Credentials, len(i.Credentials))
|
|
|
|
for ct, original := range i.Credentials {
|
|
if !slices.Contains(includeCredentials, ct) {
|
|
toPublish := original
|
|
toPublish.Config = []byte{}
|
|
credsToPublish[ct] = toPublish
|
|
continue
|
|
}
|
|
|
|
switch ct {
|
|
case CredentialsTypeOIDC, CredentialsTypeSAML:
|
|
toPublish := original
|
|
toPublish.Config = []byte{}
|
|
|
|
var i int
|
|
var err error
|
|
gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool {
|
|
if ct == CredentialsTypeOIDC {
|
|
// Don't expose these for SAML
|
|
for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token"} {
|
|
key := fmt.Sprintf("%d.%s", i, token)
|
|
ciphertext := v.Get(token).String()
|
|
|
|
plaintext, decryptErr := c.Cipher(ctx).Decrypt(ctx, ciphertext)
|
|
if decryptErr != nil {
|
|
plaintext = []byte{}
|
|
}
|
|
toPublish.Config, err = sjson.SetBytes(toPublish.Config, "providers."+key, string(plaintext))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.subject", i), v.Get("subject").String())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.provider", i), v.Get("provider").String())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if org := v.Get("organization").String(); org != "" {
|
|
toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.organization", i), org)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if useAutoLink := v.Get("use_auto_link").Bool(); useAutoLink {
|
|
toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.use_auto_link", i), useAutoLink)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
i++
|
|
return true
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credsToPublish[ct] = toPublish
|
|
case CredentialsTypeDeviceAuthn:
|
|
// The deviceauthn config stores per-key PIN state, including
|
|
// `pin_secret`, the at-rest HPKE ciphertext of the user's PIN
|
|
// secret. Strip the whole `pin` object from every key before
|
|
// publishing so the admin include_credential path never leaks it.
|
|
toPublish := original
|
|
|
|
var err error
|
|
gjson.GetBytes(original.Config, "credentials").ForEach(func(key, _ gjson.Result) bool {
|
|
toPublish.Config, err = sjson.DeleteBytes(toPublish.Config, fmt.Sprintf("credentials.%d.pin", key.Int()))
|
|
return err == nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
credsToPublish[ct] = toPublish
|
|
default:
|
|
credsToPublish[ct] = original
|
|
}
|
|
}
|
|
|
|
ii := *i
|
|
ii.Credentials = credsToPublish
|
|
return &ii, nil
|
|
}
|
|
|
|
func (i *Identity) deleteCredentialPassword() error {
|
|
cred, ok := i.GetCredentials(CredentialsTypePassword)
|
|
if !ok {
|
|
return errors.WithStack(herodot.ErrNotFound().WithReasonf("You tried to remove a password credential but this user has no such credential set up."))
|
|
}
|
|
cred.Config = []byte("{}")
|
|
i.SetCredentials(CredentialsTypePassword, *cred)
|
|
return nil
|
|
}
|
|
|
|
func (i *Identity) deleteCredentialWebAuthFromIdentity() error {
|
|
cred, ok := i.GetCredentials(CredentialsTypeWebAuthn)
|
|
if !ok {
|
|
// This should never happend as it's checked earlier in the code;
|
|
// But we never know...
|
|
return errors.WithStack(herodot.ErrNotFound().WithReasonf("You tried to remove a WebAuthn credential but this user has no such credential set up."))
|
|
}
|
|
|
|
var cc CredentialsWebAuthnConfig
|
|
if err := json.Unmarshal(cred.Config, &cc); err != nil {
|
|
// Database has been tampered or the json schema are incompatible (migration issue);
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error()))
|
|
}
|
|
|
|
updated := make([]CredentialWebAuthn, 0)
|
|
for k, cred := range cc.Credentials {
|
|
if cred.IsPasswordless {
|
|
updated = append(updated, cc.Credentials[k])
|
|
}
|
|
}
|
|
|
|
if len(updated) == 0 {
|
|
i.DeleteCredentialsType(CredentialsTypeWebAuthn)
|
|
return nil
|
|
}
|
|
|
|
cc.Credentials = updated
|
|
message, err := json.Marshal(cc)
|
|
if err != nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))
|
|
}
|
|
|
|
cred.Config = message
|
|
i.SetCredentials(CredentialsTypeWebAuthn, *cred)
|
|
return nil
|
|
}
|
|
|
|
func (i *Identity) deleteCredentialOIDCSAMLFromIdentity(ct CredentialsType, identifierToDelete string) error {
|
|
switch ct {
|
|
case CredentialsTypeOIDC, CredentialsTypeSAML:
|
|
// ok
|
|
default:
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unexpected credential type encountered: got %q, expected [%s, %s]", ct, CredentialsTypeOIDC, CredentialsTypeSAML))
|
|
}
|
|
if identifierToDelete == "" {
|
|
return errors.WithStack(herodot.ErrBadRequest().WithReasonf("You must provide an identifier to delete this credential."))
|
|
}
|
|
_, hasOIDC := i.GetCredentials(ct)
|
|
if !hasOIDC {
|
|
return errors.WithStack(herodot.ErrNotFound().WithReasonf("You tried to remove a %s credential but this user has no such credential set up.", ct))
|
|
}
|
|
var oidcConfig CredentialsOIDC
|
|
creds, err := i.ParseCredentials(ct, &oidcConfig)
|
|
if err != nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error()))
|
|
}
|
|
|
|
updatedIdentifiers := make([]string, 0, len(oidcConfig.Providers))
|
|
updatedProviders := make([]CredentialsOIDCProvider, 0, len(oidcConfig.Providers))
|
|
var found bool
|
|
for _, cfg := range oidcConfig.Providers {
|
|
if identifierToDelete == OIDCUniqueID(cfg.Provider, cfg.Subject) {
|
|
found = true
|
|
continue
|
|
}
|
|
updatedIdentifiers = append(updatedIdentifiers, OIDCUniqueID(cfg.Provider, cfg.Subject))
|
|
updatedProviders = append(updatedProviders, cfg)
|
|
}
|
|
if !found {
|
|
return errors.WithStack(herodot.ErrNotFound().WithReasonf("The identifier `%s` was not found among OIDC credentials.", identifierToDelete))
|
|
}
|
|
creds.Identifiers = updatedIdentifiers
|
|
creds.Config, err = json.Marshal(&CredentialsOIDC{Providers: updatedProviders})
|
|
if err != nil {
|
|
return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))
|
|
}
|
|
i.Credentials[ct] = *creds
|
|
return nil
|
|
}
|
|
|
|
// Patch Identities Parameters
|
|
//
|
|
// swagger:parameters batchPatchIdentities
|
|
//
|
|
//nolint:deadcode,unused
|
|
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
|
type batchPatchIdentitites struct {
|
|
// in: body
|
|
Body BatchPatchIdentitiesBody
|
|
}
|
|
|
|
// Patch Identities Body
|
|
//
|
|
// swagger:model patchIdentitiesBody
|
|
//
|
|
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
|
type BatchPatchIdentitiesBody struct {
|
|
// Identities holds the list of patches to apply
|
|
//
|
|
// required
|
|
Identities []*BatchIdentityPatch `json:"identities"`
|
|
|
|
// Future fields:
|
|
// RemotePatchesURL string
|
|
// Async bool
|
|
}
|
|
|
|
// Payload for patching an identity
|
|
//
|
|
// swagger:model identityPatch
|
|
//
|
|
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
|
type BatchIdentityPatch struct {
|
|
// The identity to create.
|
|
Create *CreateIdentityBody `json:"create"`
|
|
|
|
// The ID of this patch.
|
|
//
|
|
// The patch ID is optional. If specified, the ID will be returned in the
|
|
// response, so consumers of this API can correlate the response with the
|
|
// patch.
|
|
ID *uuid.UUID `json:"patch_id"`
|
|
}
|
|
|
|
// swagger:enum BatchPatchAction
|
|
//
|
|
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
|
type BatchPatchAction string
|
|
|
|
const (
|
|
// Create this identity.
|
|
ActionCreate BatchPatchAction = "create"
|
|
|
|
// Error indicates that the patch failed.
|
|
ActionError BatchPatchAction = "error"
|
|
|
|
// Future actions:
|
|
//
|
|
// Delete this identity.
|
|
// ActionDelete BatchPatchAction = "delete"
|
|
//
|
|
// ActionUpdate BatchPatchAction = "update"
|
|
)
|
|
|
|
// Patch identities response
|
|
//
|
|
// swagger:model batchPatchIdentitiesResponse
|
|
//
|
|
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
|
type batchPatchIdentitiesResponse struct {
|
|
// The patch responses for the individual identities.
|
|
Identities []*BatchIdentityPatchResponse `json:"identities"`
|
|
}
|
|
|
|
// Response for a single identity patch
|
|
//
|
|
// swagger:model identityPatchResponse
|
|
//
|
|
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
|
type BatchIdentityPatchResponse struct {
|
|
// The action for this specific patch
|
|
Action BatchPatchAction `json:"action"`
|
|
|
|
// The identity ID payload of this patch
|
|
IdentityID *uuid.UUID `json:"identity,omitempty"`
|
|
|
|
// The ID of this patch response, if an ID was specified in the patch.
|
|
PatchID *uuid.UUID `json:"patch_id,omitempty"`
|
|
|
|
// The error message, if the action was "error".
|
|
Error *herodot.DefaultError `json:"error,omitempty"`
|
|
}
|