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:
558
selfservice/flow/settings/hook.go
Normal file
558
selfservice/flow/settings/hook.go
Normal file
@@ -0,0 +1,558 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
stderrors "errors"
|
||||
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
"github.com/ory/kratos/x/redir"
|
||||
|
||||
"github.com/ory/pop/v6"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
"github.com/ory/x/sqlxx"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/ory/kratos/x/events"
|
||||
|
||||
"github.com/ory/kratos/session"
|
||||
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/ui/container"
|
||||
"github.com/ory/kratos/ui/node"
|
||||
|
||||
"github.com/ory/kratos/schema"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/kratos/x/transaction"
|
||||
)
|
||||
|
||||
type (
|
||||
PreHookExecutorParams struct {
|
||||
Flow *Flow
|
||||
Session *session.Session
|
||||
}
|
||||
PreHookExecutor interface {
|
||||
ExecuteSettingsPreHook(w http.ResponseWriter, r *http.Request, params PreHookExecutorParams) error
|
||||
}
|
||||
PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, params PreHookExecutorParams) error
|
||||
|
||||
PostHookPrePersistExecutorParams struct {
|
||||
Flow *Flow
|
||||
Identity *identity.Identity
|
||||
Session *session.Session
|
||||
}
|
||||
PostHookPrePersistExecutor interface {
|
||||
ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, params PostHookPrePersistExecutorParams) error
|
||||
}
|
||||
PostHookPrePersistExecutorFunc func(w http.ResponseWriter, r *http.Request, params PostHookPrePersistExecutorParams) error
|
||||
|
||||
PostHookPostPersistExecutorParams struct {
|
||||
Flow *Flow
|
||||
Previous *identity.Identity
|
||||
Updated *identity.Identity
|
||||
Session *session.Session
|
||||
}
|
||||
PostHookPostPersistExecutor interface {
|
||||
ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, params PostHookPostPersistExecutorParams) error
|
||||
}
|
||||
PostHookPostPersistExecutorFunc func(w http.ResponseWriter, r *http.Request, params PostHookPostPersistExecutorParams) error
|
||||
|
||||
HooksProvider interface {
|
||||
PreSettingsHooks(ctx context.Context) ([]PreHookExecutor, error)
|
||||
PostSettingsPrePersistHooks(ctx context.Context, settingsType string) ([]PostHookPrePersistExecutor, error)
|
||||
PostSettingsPostPersistHooks(ctx context.Context, settingsType string) ([]PostHookPostPersistExecutor, error)
|
||||
}
|
||||
|
||||
executorDependencies interface {
|
||||
identity.ManagementProvider
|
||||
identity.ValidationProvider
|
||||
identity.PoolProvider
|
||||
identity.PrivilegedPoolProvider
|
||||
identity.PendingTraitsChangePersistenceProvider
|
||||
session.ManagementProvider
|
||||
session.PersistenceProvider
|
||||
config.Provider
|
||||
|
||||
HandlerProvider
|
||||
HooksProvider
|
||||
FlowPersistenceProvider
|
||||
|
||||
nosurfx.CSRFTokenGeneratorProvider
|
||||
logrusx.Provider
|
||||
httpx.WriterProvider
|
||||
otelx.Provider
|
||||
transaction.PersistenceProvider
|
||||
}
|
||||
HookExecutor struct {
|
||||
d executorDependencies
|
||||
}
|
||||
HookExecutorProvider interface {
|
||||
SettingsHookExecutor() *HookExecutor
|
||||
}
|
||||
)
|
||||
|
||||
func (f PreHookExecutorFunc) ExecuteSettingsPreHook(w http.ResponseWriter, r *http.Request, params PreHookExecutorParams) error {
|
||||
return f(w, r, params)
|
||||
}
|
||||
|
||||
func (f PostHookPrePersistExecutorFunc) ExecuteSettingsPrePersistHook(w http.ResponseWriter, r *http.Request, params PostHookPrePersistExecutorParams) error {
|
||||
return f(w, r, params)
|
||||
}
|
||||
|
||||
func (f PostHookPostPersistExecutorFunc) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http.Request, params PostHookPostPersistExecutorParams) error {
|
||||
return f(w, r, params)
|
||||
}
|
||||
|
||||
func PostHookPostPersistExecutorNames(e []PostHookPostPersistExecutor) []string {
|
||||
names := make([]string, len(e))
|
||||
for k, ee := range e {
|
||||
names[k] = fmt.Sprintf("%T", ee)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func PostHookPrePersistExecutorNames(e []PostHookPrePersistExecutor) []string {
|
||||
names := make([]string, len(e))
|
||||
for k, ee := range e {
|
||||
names[k] = fmt.Sprintf("%T", ee)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func NewHookExecutor(d executorDependencies) *HookExecutor {
|
||||
return &HookExecutor{d: d}
|
||||
}
|
||||
|
||||
type PostSettingsHookOption func(o *postSettingsHookOptions)
|
||||
|
||||
type postSettingsHookOptions struct {
|
||||
cb func(ctxUpdate *UpdateContext) error
|
||||
}
|
||||
|
||||
func WithCallback(cb func(ctxUpdate *UpdateContext) error) func(o *postSettingsHookOptions) {
|
||||
return func(o *postSettingsHookOptions) {
|
||||
o.cb = cb
|
||||
}
|
||||
}
|
||||
|
||||
func (e *HookExecutor) handleSettingsError(_ context.Context, _ http.ResponseWriter, r *http.Request, settingsType string, f *Flow, i *identity.Identity, flowError error) error {
|
||||
if f != nil {
|
||||
if i != nil {
|
||||
var group node.UiNodeGroup
|
||||
switch settingsType {
|
||||
case "password":
|
||||
group = node.PasswordGroup
|
||||
case "oidc":
|
||||
group = node.OpenIDConnectGroup
|
||||
}
|
||||
|
||||
cont, err := container.NewFromStruct("", group, i.Traits, "traits")
|
||||
if err != nil {
|
||||
e.d.Logger().WithError(err).Error("could not update flow UI")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range cont.Nodes {
|
||||
// we only set the value and not the whole field because we want to keep types from the initial form generation
|
||||
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
|
||||
}
|
||||
}
|
||||
|
||||
if f.Type == flow.TypeBrowser {
|
||||
f.UI.SetCSRF(e.d.GenerateCSRFToken(r))
|
||||
}
|
||||
}
|
||||
|
||||
return flowError
|
||||
}
|
||||
|
||||
func (e *HookExecutor) PostSettingsHook(ctx context.Context, w http.ResponseWriter, r *http.Request, settingsType string, ctxUpdate *UpdateContext, i *identity.Identity, opts ...PostSettingsHookOption) (err error) {
|
||||
ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.HookExecutor.PostSettingsHook")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
e.d.Logger().
|
||||
WithRequest(r).
|
||||
WithField("identity_id", i.ID).
|
||||
WithField("flow_method", settingsType).
|
||||
Debug("Running PostSettingsPrePersistHooks.")
|
||||
|
||||
// Verify the redirect URL before we do any other processing.
|
||||
c := e.d.Config()
|
||||
returnTo, err := redir.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(ctx),
|
||||
redir.SecureRedirectUseSourceURL(ctxUpdate.Flow.RequestURL),
|
||||
redir.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(ctx)),
|
||||
redir.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(ctx)),
|
||||
redir.SecureRedirectOverrideDefaultReturnTo(
|
||||
e.d.Config().SelfServiceFlowSettingsReturnTo(ctx, settingsType,
|
||||
ctxUpdate.Flow.AppendTo(e.d.Config().SelfServiceFlowSettingsUI(ctx)))),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hookOptions := new(postSettingsHookOptions)
|
||||
for _, f := range opts {
|
||||
f(hookOptions)
|
||||
}
|
||||
|
||||
preHooks, err := e.d.PostSettingsPrePersistHooks(ctx, settingsType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, executor := range preHooks {
|
||||
logFields := logrus.Fields{
|
||||
"executor": fmt.Sprintf("%T", executor),
|
||||
"executor_position": k,
|
||||
"executors": PostHookPrePersistExecutorNames(preHooks),
|
||||
"identity_id": i.ID,
|
||||
"flow_method": settingsType,
|
||||
}
|
||||
|
||||
if err := executor.ExecuteSettingsPrePersistHook(w, r, PostHookPrePersistExecutorParams{
|
||||
Flow: ctxUpdate.Flow,
|
||||
Identity: i,
|
||||
Session: ctxUpdate.Session,
|
||||
}); err != nil {
|
||||
if errors.Is(err, ErrHookAbortFlow) {
|
||||
e.d.Logger().WithRequest(r).WithFields(logFields).
|
||||
Debug("A ExecuteSettingsPrePersistHook hook aborted early.")
|
||||
return nil
|
||||
}
|
||||
var group node.UiNodeGroup
|
||||
switch settingsType {
|
||||
case "password":
|
||||
group = node.PasswordGroup
|
||||
case "oidc":
|
||||
group = node.OpenIDConnectGroup
|
||||
}
|
||||
return flow.HandleHookError(w, r, ctxUpdate.Flow, i.Traits, group, err, e.d, e.d)
|
||||
}
|
||||
|
||||
e.d.Logger().WithRequest(r).WithFields(logFields).Debug("ExecuteSettingsPrePersistHook completed successfully.")
|
||||
}
|
||||
|
||||
options := []identity.ManagerOption{identity.ManagerExposeValidationErrorsForInternalTypeAssertion}
|
||||
if e.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) {
|
||||
options = append(options, identity.ManagerAllowWriteProtectedTraits)
|
||||
}
|
||||
if len(ctxUpdate.excludeCredentialTypesFromUpdate) > 0 {
|
||||
options = append(options, identity.ManagerWithoutCredentialTypes(ctxUpdate.excludeCredentialTypesFromUpdate...))
|
||||
}
|
||||
|
||||
if err := e.d.IdentityManager().Update(ctx, i, options...); err != nil {
|
||||
if errors.Is(err, identity.ErrProtectedFieldModified()) {
|
||||
e.d.Logger().WithError(err).Debug("Modifying protected field requires re-authentication.")
|
||||
return errors.WithStack(NewFlowNeedsReAuth())
|
||||
}
|
||||
if errors.Is(err, sqlcon.ErrUniqueViolation()) {
|
||||
return schema.NewDuplicateCredentialsError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.d.Logger().
|
||||
WithRequest(r).
|
||||
WithField("identity_id", i.ID).
|
||||
Debug("An identity's settings have been updated.")
|
||||
|
||||
ctxUpdate.UpdateIdentity(i)
|
||||
ctxUpdate.Flow.State = flow.StateSuccess
|
||||
if hookOptions.cb != nil {
|
||||
if err := hookOptions.cb(ctxUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
newFlow, err := e.d.SettingsHandler().NewFlow(ctx, w, r, i, ctxUpdate.Session, ctxUpdate.Flow.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctxUpdate.Flow.UI = newFlow.UI
|
||||
ctxUpdate.Flow.UI.ResetMessages()
|
||||
ctxUpdate.Flow.UI.AddMessage(node.DefaultGroup, text.NewInfoSelfServiceSettingsUpdateSuccess())
|
||||
ctxUpdate.Flow.InternalContext = newFlow.InternalContext
|
||||
if err := e.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postHooks, err := e.d.PostSettingsPostPersistHooks(ctx, settingsType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
previous := ctxUpdate.GetSessionIdentity()
|
||||
for k, executor := range postHooks {
|
||||
if err := executor.ExecuteSettingsPostPersistHook(w, r, PostHookPostPersistExecutorParams{
|
||||
Flow: ctxUpdate.Flow,
|
||||
Previous: previous,
|
||||
Updated: i,
|
||||
Session: ctxUpdate.Session,
|
||||
}); err != nil {
|
||||
if errors.Is(err, ErrHookAbortFlow) {
|
||||
e.d.Logger().
|
||||
WithRequest(r).
|
||||
WithField("executor", fmt.Sprintf("%T", executor)).
|
||||
WithField("executor_position", k).
|
||||
WithField("executors", PostHookPostPersistExecutorNames(postHooks)).
|
||||
WithField("identity_id", i.ID).
|
||||
WithField("flow_method", settingsType).
|
||||
Debug("A ExecuteSettingsPostPersistHook hook aborted early.")
|
||||
return nil
|
||||
}
|
||||
return e.handleSettingsError(ctx, w, r, settingsType, ctxUpdate.Flow, i, err)
|
||||
}
|
||||
|
||||
e.d.Logger().WithRequest(r).
|
||||
WithField("executor", fmt.Sprintf("%T", executor)).
|
||||
WithField("executor_position", k).
|
||||
WithField("executors", PostHookPostPersistExecutorNames(postHooks)).
|
||||
WithField("identity_id", i.ID).
|
||||
WithField("flow_method", settingsType).
|
||||
Debug("ExecuteSettingsPostPersistHook completed successfully.")
|
||||
}
|
||||
|
||||
e.d.Logger().
|
||||
WithRequest(r).
|
||||
WithField("identity_id", i.ID).
|
||||
WithField("flow_method", settingsType).
|
||||
Debug("Completed all PostSettingsPrePersistHooks and PostSettingsPostPersistHooks.")
|
||||
|
||||
trace.SpanFromContext(ctx).AddEvent(events.NewSettingsSucceeded(
|
||||
ctx, ctxUpdate.Flow.ID, i.ID, string(ctxUpdate.Flow.Type), settingsType))
|
||||
|
||||
if ctxUpdate.Flow.Type == flow.TypeAPI {
|
||||
updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(ctx, ctxUpdate.Flow.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ContinueWith items are transient items, not stored in the database, and need to be carried over here, so
|
||||
// they can be returned to the client.
|
||||
updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems
|
||||
|
||||
e.d.Writer().Write(w, r, updatedFlow)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := e.d.SessionManager().IssueCookie(ctx, w, r, ctxUpdate.Session); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if x.IsJSONRequest(r) {
|
||||
updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(ctx, ctxUpdate.Flow.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ContinueWith items are transient items, not stored in the database, and need to be carried over here, so
|
||||
// they can be returned to the client.
|
||||
ctxUpdate.Flow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
|
||||
updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems
|
||||
|
||||
e.d.Writer().Write(w, r, updatedFlow)
|
||||
return nil
|
||||
}
|
||||
|
||||
redir.ContentNegotiationRedirection(w, r, i.CopyWithoutCredentials(), e.d.Writer(), returnTo.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// settingsTypePendingTraitsChange is the settings strategy the verify_new_address
|
||||
// hook is allowed to be configured under. The JSON config schema
|
||||
// (selfServiceAfterSettingsProfileMethod) only admits verify_new_address inside
|
||||
// the profile hook list, so a pending traits change can only ever originate
|
||||
// from the profile strategy.
|
||||
const settingsTypePendingTraitsChange = "profile"
|
||||
|
||||
// ErrPendingTraitsChangeSessionInvalid is returned by ApplyPendingTraitsChange
|
||||
// when the session that created the pending change is missing, revoked,
|
||||
// expired, disabled, or belongs to a different tenant. Callers should treat
|
||||
// this as "the verification attempt is invalid" — the change is not applied.
|
||||
var ErrPendingTraitsChangeSessionInvalid = stderrors.New("pending traits change session is no longer valid")
|
||||
|
||||
// ApplyPendingTraitsChange commits a pending traits change to the identity and
|
||||
// fires the "after profile settings, post-persist" hook chain. It is the
|
||||
// single entry point verification strategies use to resolve a pending change
|
||||
// created by a settings flow.
|
||||
//
|
||||
// The originating session is validated inside the same transaction that
|
||||
// applies the change, so a session that is revoked between the caller's
|
||||
// token/code consumption and this call is still rejected. If validation
|
||||
// succeeds, the loaded session is reused for the hook chain — no second
|
||||
// GetSession round-trip.
|
||||
//
|
||||
// Returns ErrPendingTraitsChangeSessionInvalid when the session is not usable.
|
||||
// Returns identity.ErrConcurrentModification when the identity's traits have
|
||||
// drifted since the change was proposed.
|
||||
func (e *HookExecutor) ApplyPendingTraitsChange(
|
||||
ctx context.Context,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
ptc *identity.PendingTraitsChange,
|
||||
) (i *identity.Identity, err error) {
|
||||
ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.HookExecutor.ApplyPendingTraitsChange")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
// Reject PTCs whose FK references were nulled or never written. The
|
||||
// session FK is ON DELETE SET NULL and the origin-flow FK is ON
|
||||
// DELETE CASCADE, so a NULL here means either a revoked session or
|
||||
// a writer that failed to set the link.
|
||||
if !ptc.SessionID.Valid || !ptc.OriginSettingsFlowID.Valid {
|
||||
return nil, errors.WithStack(ErrPendingTraitsChangeSessionInvalid)
|
||||
}
|
||||
|
||||
var (
|
||||
sess *session.Session
|
||||
// Capture the pre-change identity before sess.Identity is overwritten
|
||||
// below so post-persist hooks (notify_previous_addresses) can compute
|
||||
// diffs without re-reading the row.
|
||||
previous *identity.Identity
|
||||
)
|
||||
err = e.d.TransactionalPersisterProvider().Transaction(ctx, func(ctx context.Context, _ *pop.Connection) error {
|
||||
// ExpandDefault loads the session's identity in the same query,
|
||||
// so IsActive() below checks the identity's active state and we
|
||||
// avoid a second round-trip to IdentityPool.GetIdentity for the
|
||||
// hash comparison.
|
||||
s, sErr := e.d.SessionPersister().GetSession(ctx, ptc.SessionID.UUID, session.ExpandDefault)
|
||||
if sErr != nil {
|
||||
if errors.Is(sErr, sqlcon.ErrNoRows()) {
|
||||
return errors.WithStack(ErrPendingTraitsChangeSessionInvalid)
|
||||
}
|
||||
return sErr
|
||||
}
|
||||
if s.NID != ptc.NID || !s.IsActive() {
|
||||
return errors.WithStack(ErrPendingTraitsChangeSessionInvalid)
|
||||
}
|
||||
// TODO(jonas): We can likely drop the identity id column entirely, since the session's identity is always loaded and we check the ID matches here. This would simplify the schema and remove a potential source of FK inconsistency.
|
||||
if s.Identity == nil || s.Identity.ID != ptc.IdentityID {
|
||||
return errors.WithStack(ErrPendingTraitsChangeSessionInvalid)
|
||||
}
|
||||
previous = s.Identity
|
||||
sess = s
|
||||
|
||||
if identity.HashTraits(json.RawMessage(s.Identity.Traits)) != ptc.OriginalTraitsHash {
|
||||
return identity.ErrConcurrentModification
|
||||
}
|
||||
|
||||
if err := e.d.IdentityManager().UpdateTraits(
|
||||
ctx, ptc.IdentityID, identity.Traits(ptc.ProposedTraits),
|
||||
identity.ManagerAllowWriteProtectedTraits,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The PTC Status column is informational only; the real
|
||||
// single-submit guard is the verification flow state machine
|
||||
// (StatePassedChallenge) plus atomic token/code consumption.
|
||||
// We intentionally do not write Status here.
|
||||
|
||||
// Re-read the identity after UpdateTraits so the returned
|
||||
// snapshot reflects the freshly-committed state, including any
|
||||
// VerifiableAddress row UpdateTraits may have created for the
|
||||
// new address.
|
||||
updated, err := e.d.IdentityPool().GetIdentity(ctx, ptc.IdentityID, identity.ExpandDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx := range updated.VerifiableAddresses {
|
||||
a := &updated.VerifiableAddresses[idx]
|
||||
if a.Value == ptc.NewAddressValue && a.Via == ptc.NewAddressVia {
|
||||
a.Verified = true
|
||||
verifiedAt := sqlxx.NullTime(time.Now().UTC())
|
||||
a.VerifiedAt = &verifiedAt
|
||||
a.Status = identity.VerifiableAddressStatusCompleted
|
||||
if err := e.d.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, a, "verified", "verified_at", "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i = updated
|
||||
// Keep Session.Identity in sync so post-persist webhook templates render the committed traits and the newly-verified address.
|
||||
// TODO: pointer aliasing could cause issues if a downstream hook implementation modifies the identity in-place instead of treating it as immutable. We should either enforce immutability or document this behavior.
|
||||
sess.Identity = updated
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fire the "profile" post-persist hooks. Load the originating settings flow
|
||||
// so the hook chain receives a real Flow (honest ID, type, RequestURL)
|
||||
// instead of a synthetic one. The FK is ON DELETE CASCADE, so if the flow
|
||||
// was cleaned up the PTC was already deleted and we would not reach this
|
||||
// call. Apply has already committed, so a hook error cannot roll the change
|
||||
// back — we propagate errors so callers can surface them in the
|
||||
// verification UI.
|
||||
originFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(ctx, ptc.OriginSettingsFlowID.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
postHooks, err := e.d.PostSettingsPostPersistHooks(ctx, settingsTypePendingTraitsChange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, executor := range postHooks {
|
||||
logFields := logrus.Fields{
|
||||
"executor": fmt.Sprintf("%T", executor),
|
||||
"executor_position": k,
|
||||
"executors": PostHookPostPersistExecutorNames(postHooks),
|
||||
"identity_id": i.ID,
|
||||
"flow_method": settingsTypePendingTraitsChange,
|
||||
}
|
||||
if err := executor.ExecuteSettingsPostPersistHook(w, r, PostHookPostPersistExecutorParams{
|
||||
Flow: originFlow,
|
||||
Previous: previous,
|
||||
Updated: i,
|
||||
Session: sess,
|
||||
}); err != nil {
|
||||
if errors.Is(err, ErrHookAbortFlow) {
|
||||
e.d.Logger().WithRequest(r).WithFields(logFields).
|
||||
Debug("A post-persist hook aborted the pending-traits-change continuation flow.")
|
||||
return i, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
e.d.Logger().WithRequest(r).WithFields(logFields).
|
||||
Debug("ExecuteSettingsPostPersistHook completed successfully after pending traits change.")
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (e *HookExecutor) PreSettingsHook(ctx context.Context, w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) (err error) {
|
||||
ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.flow.settings.HookExecutor.PreSettingsHook")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
hooks, err := e.d.PreSettingsHooks(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, executor := range hooks {
|
||||
if err := executor.ExecuteSettingsPreHook(w, r, PreHookExecutorParams{
|
||||
Flow: a,
|
||||
Session: s,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user