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
559 lines
19 KiB
Go
559 lines
19 KiB
Go
// 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
|
|
}
|