snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
757
identity/identity.go
Normal file
757
identity/identity.go
Normal file
@@ -0,0 +1,757 @@
|
||||
// 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user