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:
113
selfservice/flow/settings/organizations.go
Normal file
113
selfservice/flow/settings/organizations.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/x"
|
||||
)
|
||||
|
||||
var organizationFilter = []StrategyFilter{func(s Strategy) bool {
|
||||
a, b := s.(flow.OrganizationImplementor)
|
||||
return b && a.SupportsOrganizations()
|
||||
}}
|
||||
|
||||
// PrepareOrganizations resolves the org id from the identity (winner) or the
|
||||
// `?organization=` query param, sets it on the flow, and returns a filter
|
||||
// that keeps only org-implementing strategies. Returns an empty filter when
|
||||
// no org id applies — the flow then proceeds as a normal (non-org) settings
|
||||
// flow.
|
||||
//
|
||||
// The identity is the authoritative source: an identity already bound to an
|
||||
// org cannot be silently re-bound, and the session cache must not be relied
|
||||
// on because it can lag behind a just-persisted org binding (PostSettingsHook
|
||||
// rebuilds the flow with the freshly linked identity).
|
||||
//
|
||||
// The query param is free-form caller input, so it is only honored when one
|
||||
// of the identity's email addresses is under the organization's configured
|
||||
// domains. Without that gate, any authenticated identity could render an
|
||||
// org-scoped flow for every org in the project and enumerate its SSO
|
||||
// providers. The org hooks block self-service registration and profile
|
||||
// updates under a claimed domain, so a matching address implies the domain
|
||||
// owner provisioned the account.
|
||||
//
|
||||
// When the resolved org id does not match any configured organization, the
|
||||
// flow falls back to the unscoped path. An identity may carry an
|
||||
// organization_id for an organization that has since been deleted; scoping
|
||||
// to a dangling org would render no nodes and trap chained flows such as
|
||||
// recovery -> settings.
|
||||
func PrepareOrganizations(r *http.Request, f *Flow, i *identity.Identity, configuredOrgs []config.Organization) []StrategyFilter {
|
||||
if f.OrganizationID.Valid {
|
||||
return organizationFilter
|
||||
}
|
||||
|
||||
if i != nil && i.OrganizationID.Valid {
|
||||
if _, ok := findConfiguredOrganization(i.OrganizationID.UUID, configuredOrgs); !ok {
|
||||
return []StrategyFilter{}
|
||||
}
|
||||
f.OrganizationID = i.OrganizationID
|
||||
return organizationFilter
|
||||
}
|
||||
|
||||
orgID := flow.ParseOrganizationFromURLQuery(r.Context(), r.URL.Query())
|
||||
if !orgID.Valid {
|
||||
return []StrategyFilter{}
|
||||
}
|
||||
|
||||
org, ok := findConfiguredOrganization(orgID.UUID, configuredOrgs)
|
||||
if !ok {
|
||||
return []StrategyFilter{}
|
||||
}
|
||||
|
||||
if !identityEmailMatchesOrganizationDomains(i, org) {
|
||||
return []StrategyFilter{}
|
||||
}
|
||||
|
||||
f.OrganizationID = orgID
|
||||
return organizationFilter
|
||||
}
|
||||
|
||||
func findConfiguredOrganization(id uuid.UUID, configured []config.Organization) (config.Organization, bool) {
|
||||
idx := slices.IndexFunc(configured, func(o config.Organization) bool {
|
||||
return o.ID == id
|
||||
})
|
||||
if idx < 0 {
|
||||
return config.Organization{}, false
|
||||
}
|
||||
return configured[idx], true
|
||||
}
|
||||
|
||||
// identityEmailMatchesOrganizationDomains reports whether one of the
|
||||
// identity's email addresses is under one of the organization's domains. The
|
||||
// normalization mirrors the org email-domain matching in the cloud module's
|
||||
// organization hook.
|
||||
func identityEmailMatchesOrganizationDomains(i *identity.Identity, org config.Organization) bool {
|
||||
if i == nil {
|
||||
return false
|
||||
}
|
||||
// TODO(jonas): This should actually use the `organization.via` field of the identity schema.
|
||||
for _, addr := range i.VerifiableAddresses {
|
||||
if addr.Via != identity.AddressTypeEmail {
|
||||
continue
|
||||
}
|
||||
_, domain, ok := strings.Cut(x.NormalizeEmailIdentifier(addr.Value), "@")
|
||||
if !ok || domain == "" {
|
||||
continue
|
||||
}
|
||||
if slices.ContainsFunc(org.Domains, func(orgDomain string) bool {
|
||||
return strings.EqualFold(domain, x.NormalizeOtherIdentifier(orgDomain))
|
||||
}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user