Files
upstream-ory-kratos/identity/identity.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

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"`
}