Files
upstream-ory-kratos/selfservice/hook/notify_previous_addresses.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

149 lines
4.0 KiB
Go

// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hook
import (
"context"
"net/http"
"slices"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/x/logrusx"
"github.com/ory/x/otelx"
)
const (
RecipientsRemoved = "removed"
RecipientsAllVerified = "all_verified"
RecipientsAll = "all"
)
type (
NotifyPreviousAddressesConfig struct {
Recipients string `json:"recipients"`
}
notifyPreviousAddressesDependencies interface {
identity.ManagementProvider
config.Provider
logrusx.Provider
otelx.Provider
}
NotifyPreviousAddresses struct {
r notifyPreviousAddressesDependencies
c *NotifyPreviousAddressesConfig
}
)
var _ settings.PostHookPostPersistExecutor = new(NotifyPreviousAddresses)
func NewNotifyPreviousAddresses(r notifyPreviousAddressesDependencies, c *NotifyPreviousAddressesConfig) *NotifyPreviousAddresses {
if c == nil {
c = &NotifyPreviousAddressesConfig{}
}
if c.Recipients == "" {
c.Recipients = RecipientsRemoved
}
return &NotifyPreviousAddresses{r: r, c: c}
}
func (e *NotifyPreviousAddresses) ExecuteSettingsPostPersistHook(
_ http.ResponseWriter, r *http.Request,
params settings.PostHookPostPersistExecutorParams,
) error {
return otelx.WithSpan(r.Context(), "selfservice.hook.NotifyPreviousAddresses.ExecuteSettingsPostPersistHook", func(ctx context.Context) error {
if params.Previous == nil || params.Updated == nil {
return nil
}
targets := computeNotifyTargets(e.c.Recipients, params.Previous, params.Updated)
targets = slices.DeleteFunc(targets, func(a identity.AddressRef) bool {
return a.Via != identity.AddressTypeEmail && a.Via != identity.AddressTypeSMS
})
if len(targets) == 0 {
return nil
}
if err := e.r.IdentityManager().SendVerifiableAddressChangedNotifications(ctx, targets, params.Updated); err != nil {
e.r.Logger().WithError(err).
WithField("count", len(targets)).
Warn("Failed to queue one or more verifiable-address-change notifications.")
// Persist already succeeded — never fail the flow on courier errors.
}
return nil
})
}
func computeNotifyTargets(mode string, original, proposed *identity.Identity) []identity.AddressRef {
if !addressesChanged(original.VerifiableAddresses, proposed.VerifiableAddresses) {
return nil
}
switch mode {
case RecipientsAllVerified:
out := make([]identity.AddressRef, 0, len(original.VerifiableAddresses))
for _, a := range original.VerifiableAddresses {
if a.Verified {
out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via})
}
}
return out
case RecipientsAll:
out := make([]identity.AddressRef, 0, len(original.VerifiableAddresses))
for _, a := range original.VerifiableAddresses {
out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via})
}
return out
default: // RecipientsRemoved
keep := map[string]struct{}{}
for _, a := range proposed.VerifiableAddresses {
keep[a.Via+"|"+a.Value] = struct{}{}
}
out := make([]identity.AddressRef, 0)
for _, a := range original.VerifiableAddresses {
if _, still := keep[a.Via+"|"+a.Value]; still {
continue
}
out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via})
}
return out
}
}
type set struct {
m map[string]struct{}
}
func newSet() *set {
return &set{m: make(map[string]struct{})}
}
func (s *set) add(via, value string) {
s.m[via+"|"+value] = struct{}{}
}
func (s *set) contains(via, value string) bool {
_, ok := s.m[via+"|"+value]
return ok
}
// addressesChanged reports whether two verifiable-address slices represent
// different (via,value) sets. Status or verification-flag flips alone are not
// "address changes" and must not trigger notifications.
func addressesChanged(a, b []identity.VerifiableAddress) bool {
if len(a) != len(b) {
return true
}
seen := newSet()
for _, x := range a {
seen.add(x.Via, x.Value)
}
for _, x := range b {
if !seen.contains(x.Via, x.Value) {
return true
}
}
return false
}