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
121 lines
4.5 KiB
Go
121 lines
4.5 KiB
Go
// Copyright © 2026 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package identity
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
|
|
"github.com/ory/x/region"
|
|
)
|
|
|
|
const (
|
|
PendingTraitsChangeStatusPending = "pending"
|
|
PendingTraitsChangeStatusCompleted = "completed"
|
|
)
|
|
|
|
// PendingTraitsChange stores a deferred traits update that is waiting
|
|
// for the new address to be verified before being applied to the identity.
|
|
//
|
|
// swagger:ignore
|
|
type PendingTraitsChange struct {
|
|
// ID is the unique identifier of this pending change.
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
|
|
// IdentityID is the identity this change belongs to.
|
|
IdentityID uuid.UUID `json:"identity_id" db:"identity_id"`
|
|
|
|
// IdentityRegion optionally carries the already-known region of the
|
|
// identity the change belongs to, so persisters can use it without
|
|
// re-reading the identity row. It is never persisted.
|
|
IdentityRegion region.Region `json:"-" db:"-" faker:"-"`
|
|
|
|
// NID is the network ID (multi-tenant discriminator).
|
|
NID uuid.UUID `json:"-" db:"nid"`
|
|
|
|
// NewAddressValue is the new address value being verified (e.g., "new@example.com").
|
|
NewAddressValue string `json:"new_address_value" db:"new_address_value"`
|
|
|
|
// NewAddressVia is the delivery channel for the new address ("email" or "sms").
|
|
NewAddressVia string `json:"new_address_via" db:"new_address_via"`
|
|
|
|
// OriginalTraitsHash is the SHA-256 hash of the identity's traits at the time
|
|
// the pending change was created. Used to detect concurrent modifications.
|
|
OriginalTraitsHash string `json:"original_traits_hash" db:"original_traits_hash"`
|
|
|
|
// ProposedTraits is the full traits JSON snapshot to apply on successful verification.
|
|
ProposedTraits json.RawMessage `json:"proposed_traits" db:"proposed_traits"`
|
|
|
|
// VerificationFlowID links this pending change to the verification flow that must complete.
|
|
VerificationFlowID uuid.UUID `json:"verification_flow_id" db:"verification_flow_id"`
|
|
|
|
// Status is the current state: "pending" or "completed".
|
|
Status string `json:"status" db:"status"`
|
|
|
|
// SessionID is the session that created this pending change. Used at apply
|
|
// time to verify the session is still active and match it to the current NID.
|
|
// Nullable: ON DELETE SET NULL in the schema — a NULL value means the
|
|
// session was revoked or deleted and the pending change must be rejected.
|
|
SessionID uuid.NullUUID `json:"session_id,omitempty" db:"session_id"`
|
|
|
|
// OriginSettingsFlowID is the settings flow that produced this pending
|
|
// change. Used at apply time to load the real flow for the
|
|
// settings-post-persist webhook chain. Non-nullable in practice: the
|
|
// FK is declared ON DELETE CASCADE, so a missing flow deletes the PTC.
|
|
// A NULL value here means the FK was never set by a malformed writer
|
|
// and the pending change must be rejected.
|
|
OriginSettingsFlowID uuid.NullUUID `json:"origin_settings_flow_id,omitempty" db:"origin_settings_flow_id"`
|
|
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
func (PendingTraitsChange) TableName() string {
|
|
return "identity_pending_traits_changes"
|
|
}
|
|
|
|
func (p *PendingTraitsChange) ToPersistable() (*VerifiableAddress, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (p *PendingTraitsChange) Address() string {
|
|
return p.NewAddressValue
|
|
}
|
|
|
|
func (p *PendingTraitsChange) DeliveryVia() string {
|
|
return p.NewAddressVia
|
|
}
|
|
|
|
// HashTraits returns a hex-encoded SHA-256 hash of the JSON-compacted traits.
|
|
// Used to detect concurrent identity modifications between pending change
|
|
// creation and application.
|
|
func HashTraits(traits json.RawMessage) string {
|
|
var buf bytes.Buffer
|
|
if err := json.Compact(&buf, traits); err != nil {
|
|
h := sha256.Sum256(traits)
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
h := sha256.Sum256(buf.Bytes())
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
// PendingTraitsChangePersister handles CRUD for pending traits changes.
|
|
type PendingTraitsChangePersister interface {
|
|
CreatePendingTraitsChange(ctx context.Context, p *PendingTraitsChange) error
|
|
GetPendingTraitsChangeByVerificationFlow(ctx context.Context, flowID uuid.UUID) (*PendingTraitsChange, error)
|
|
DeletePendingTraitsChangesByIdentity(ctx context.Context, identityID uuid.UUID) error
|
|
UpdatePendingTraitsChange(ctx context.Context, p *PendingTraitsChange) error
|
|
}
|
|
|
|
// PendingTraitsChangePersistenceProvider provides access to the persister.
|
|
type PendingTraitsChangePersistenceProvider interface {
|
|
PendingTraitsChangePersister() PendingTraitsChangePersister
|
|
}
|