Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
60ee647
feat: add scim to sso providers, banner reason to users, and add scim…
Bewinxed Dec 10, 2025
624b762
feat: add SCIM fields and methods to models
Bewinxed Dec 10, 2025
b10f6f4
feat: add scim error codes
Bewinxed Dec 10, 2025
bfcf6aa
fix: add findUserByProvider to user model
Bewinxed Dec 10, 2025
1a1e01e
chore: update Ban() calls in admin.go
Bewinxed Dec 10, 2025
6f4ec12
feat: Add SCIM v2 endpoints
Bewinxed Dec 10, 2025
9cffbe9
feat: register SCIM endpoints
Bewinxed Dec 10, 2025
ae5a873
fix: reuse existing helpers for user creation, add audit logging
Bewinxed Dec 10, 2025
aabec76
feat: Add Admin SCIM management endpoints
Bewinxed Dec 10, 2025
5555ad4
chore: make SCIM Token prefixed
Bewinxed Dec 10, 2025
c20e432
fix: several bugfixes, add db-pagination
Bewinxed Dec 10, 2025
a2aac77
fix: restore is_super_admin to the User model
Bewinxed Dec 10, 2025
0b19686
fix: RFC 7644 compliance
Bewinxed Dec 11, 2025
f34cf8d
chore: extract scim_parser out, minimal impl
Bewinxed Dec 11, 2025
1318e1b
chore: extract scim types out
Bewinxed Dec 24, 2025
cc14674
chore: add scim2/filter-parser as SCIM query parser
Bewinxed Dec 24, 2025
ece724e
chore: add SCIM errors
Bewinxed Dec 24, 2025
9acdadd
feat: add SCIM filter support to user and group queries
Bewinxed Dec 24, 2025
f5ff513
chore: refactor SCIM to use extracted types/helpers.
Bewinxed Dec 24, 2025
b36817f
feat: add scim2/filter-parser dependency
Bewinxed Dec 24, 2025
fac2351
feat: add scim filters with RFC 7644 support
Bewinxed Dec 24, 2025
d015452
fix: group schema migration fix
Bewinxed Dec 24, 2025
546354b
chore: rename scim parser to scim helper after dep was added
Bewinxed Dec 24, 2025
6379c0b
chore: extract UserNotInSSOProviderError as typed error
Bewinxed Jan 18, 2026
d6eb027
chore(scim): remove unused group query functions
Bewinxed Jan 18, 2026
3d7aee8
chore: consolidate userBelongsToProvider implementations
Bewinxed Jan 18, 2026
814dce4
chore: consolidate filter clause types
Bewinxed Jan 18, 2026
6c5912c
fix(scim): remove duplicate types and fix error handling consistency
Bewinxed Jan 18, 2026
fa91384
chore: add scim test infrastructure
Bewinxed Jan 19, 2026
bfaf1a9
chore: add SCIM user filtering tests
Bewinxed Jan 19, 2026
a14169a
chore: add SCIM user PATCH tests
Bewinxed Jan 19, 2026
024c400
chore: add SCIM group CRUD tests
Bewinxed Jan 19, 2026
7622b7e
chore: add SCIM group filtering tests
Bewinxed Jan 19, 2026
c2a2e6b
chore: add SCIM group membership PATCH tests
Bewinxed Jan 19, 2026
886ce98
chore: add SCIM authentication and error tests
Bewinxed Jan 19, 2026
cf58d3c
chore: centralize test fixtures
Bewinxed Jan 19, 2026
f4f72d8
chore: add nosec for false positive token error code
Bewinxed Jan 19, 2026
f8d7f19
fix: use sendSCIMJSON for SCIM error responses
Bewinxed Feb 5, 2026
08166c1
fix: use schema-qualified table names in SCIM queries
Bewinxed Feb 5, 2026
9fcd5ee
fix: scimReplaceUser now validates, updates email, and fully replaces…
Bewinxed Feb 5, 2026
765f4d9
fix: handle count=0 in SCIM pagination per RFC 7644
Bewinxed Feb 5, 2026
dbad99d
fix: make SCIM user delete idempotent
Bewinxed Feb 5, 2026
3ba2dbb
fix: sanitize JSON errors and use API_EXTERNAL_URL for SCIM base URL
Bewinxed Feb 5, 2026
e2b6d37
fix: use FlexBool for SCIM user Active field
Bewinxed Feb 5, 2026
b85f055
fix: add ESCAPE clause to LIKE filters and use SCIM error types consi…
Bewinxed Feb 5, 2026
0eb41f7
chore: remove unused SCIM error code constants
Bewinxed Feb 5, 2026
68b02b1
fix: optimize SCIM token lookup and add partial index
Bewinxed Feb 5, 2026
fc3cec5
fix: batch user loading in SetMembers to avoid N+1 queries
Bewinxed Feb 5, 2026
149e2b7
fix: use API_EXTERNAL_URL for SCIM base URL in admin endpoints
Bewinxed Feb 5, 2026
b294dac
chore: add SCIM PUT replace and cross-provider isolation tests
Bewinxed Feb 5, 2026
bdb6272
fix: add request size limits and validation for SCIM endpoints
Bewinxed Feb 5, 2026
8bd64fc
fix: use SHA-256 instead of bcrypt for SCIM token lookup
Bewinxed Feb 5, 2026
e5a88c7
fix: harden SCIM cross-provider isolation, error handling, and batch …
Bewinxed Feb 6, 2026
6a9e704
chore: route all SCIM PATCH paths through filter.ParsePath
Bewinxed Feb 6, 2026
8febe1a
fix: pass ResponseWriter to MaxBytesReader instead of nil
Bewinxed Feb 6, 2026
7b0ec3f
fix: wrap all raw DB/model errors in SCIMHTTPError types
Bewinxed Feb 6, 2026
bf83439
fix: map uniqueness violations to 409 in group patch and replace
Bewinxed Feb 6, 2026
4f2bd1c
fix: validate all member IDs in SetMembers before replacing
Bewinxed Feb 6, 2026
cac9242
fix: preserve non-not-found errors in SetMembers validation and lock …
Bewinxed Feb 6, 2026
67329b3
fix: remove DISTINCT from FOR SHARE query and de-duplicate in Go
Bewinxed Feb 6, 2026
f4dde2f
fix: enable SCIM user reactivation for SSO users
Bewinxed Feb 6, 2026
9a2c3ec
fix: scope SCIM reactivation lookup by provider to prevent cross-prov…
Bewinxed Feb 6, 2026
74c8a22
fix: make SCIM reactivation deterministic by querying all matching SS…
Bewinxed Feb 6, 2026
0f5b704
fix: return 400 for unsupported SCIM PATCH paths and value types
Bewinxed Feb 6, 2026
ca7ba7b
fix: log SCIM 5xx errors at Error level and 429 at Warn level
Bewinxed Feb 6, 2026
6d197b3
fix: reject ambiguous reactivation when multiple deprovisioned users …
Bewinxed Feb 6, 2026
a43caab
fix: support SCIM PATCH add with explicit path for user attributes
Bewinxed Feb 6, 2026
8b487af
fix: cap startIndex, use SetEmail in PATCH, map externalId uniqueness…
Bewinxed Feb 6, 2026
19ac869
fix: use SetEmail consistently in SCIM PATCH/PUT and map email unique…
Bewinxed Feb 6, 2026
de7d50e
fix: enforce provider-scoped email uniqueness in SCIM PUT and PATCH p…
Bewinxed Feb 6, 2026
7d30a0d
chore: add SCIM email uniqueness regression tests for PUT and PATCH
Bewinxed Feb 6, 2026
a78bc5e
fix: fix group member pointer aliasing, group create race, and FlexBo…
Bewinxed Feb 6, 2026
df00e88
fix: preserve non-SCIM metadata in PUT and pass IP to audit logs
Bewinxed Feb 6, 2026
5462004
fix: stop clearing provider_id on externalId removal and batch group …
Bewinxed Feb 6, 2026
6e1e5c5
fix: normalize active parsing, derive externalId from identity data, …
Bewinxed Feb 6, 2026
12a31c8
fix: use identity_data for externalId filter and add row locking to A…
Bewinxed Feb 6, 2026
702b6bc
fix: lock identity rows alongside user rows in group membership valid…
Bewinxed Feb 6, 2026
0942da7
fix: honor active attribute on SCIM user create
Bewinxed Feb 6, 2026
0a1f82d
fix: check non-SSO email collisions, sync sub on externalId change, g…
Bewinxed Feb 6, 2026
008bb4d
fix: default members in group list, avoid eager loading, make timesta…
Bewinxed Feb 6, 2026
4067fc8
chore: consolidate SCIM migrations into single file
Bewinxed Feb 7, 2026
33e6d10
fix: use correct audit action when reprovisioning inactive user
Bewinxed Feb 7, 2026
bdc7652
fix: update identity data and merge metadata on user reactivation
Bewinxed Feb 7, 2026
9be5e31
fix: deduplicate member IDs before validation in AddMembers/SetMembers
Bewinxed Feb 7, 2026
964e9fd
chore: extract identity update helpers to reduce SCIM patch complexity
Bewinxed Feb 7, 2026
eaba906
feat: gate SCIM routes behind GOTRUE_SCIM_ENABLED config flag
Bewinxed Feb 7, 2026
5f25603
fix: validate schemas field in SCIM request bodies per RFC 7644
Bewinxed Feb 7, 2026
b137208
fix: use NULLIF in COALESCE to skip empty userName in filter queries
Bewinxed Feb 7, 2026
bb42801
fix: add safety LIMIT to GetMembers query
Bewinxed Feb 7, 2026
21368c0
chore: deduplicate schema and resource type definitions
Bewinxed Feb 7, 2026
5b4e06d
chore: fix indentation of SCIM route registration block
Bewinxed Feb 7, 2026
6dbdcb2
fix: reset ProviderID and sub when externalId is omitted in PUT and r…
Bewinxed Feb 7, 2026
13dcec9
fix: check cross-provider email collisions to return 409 instead of 500
Bewinxed Feb 7, 2026
9d5f8ab
fix: reset ProviderID and sub when removing externalId via PATCH
Bewinxed Feb 7, 2026
150e7a2
chore: extract shared helpers and remove duplication across SCIM files
Bewinxed Feb 7, 2026
050a7af
fix: remove IsSuperAdmin field re-added against upstream removal
Bewinxed Feb 7, 2026
7328e2e
chore: remove dead code, fix PR description, add SCIM test coverage
Bewinxed Feb 7, 2026
30d01f0
fix: update SCIM delete user response to return 404 for deprovisioned…
Bewinxed Feb 7, 2026
4b5a928
fix: remove unused identityID assignment in scimCreateUser function
Bewinxed Feb 7, 2026
129c3bd
chore: remove redundant tests, extract error constants, normalize SCI…
Bewinxed Feb 9, 2026
6b61b9e
feat: use Pop instead of raw queries and fix some tests
Bewinxed Feb 9, 2026
8c5c3a7
chore: extract shared SCIM helpers, deduplicate group member logic
Bewinxed Mar 8, 2026
025855e
fix: harden SCIM reactivation, escape LIKE patterns, gate admin route…
Bewinxed Mar 8, 2026
b43a58a
fix: fail-fast on bad input types, require SSO identity in mutation p…
Bewinxed Mar 8, 2026
e1836f9
fix: fix user selection in scim groups to not include unknown fields
Bewinxed Mar 9, 2026
aec81f7
feat: add user lock state distinct from ban
Bewinxed Jun 2, 2026
d9c58d0
feat: reject locked users across auth flows
Bewinxed Jun 2, 2026
4804c23
chore: deprovision SCIM users via lock instead of ban
Bewinxed Jun 2, 2026
e0091be
feat: support filtering SCIM users by active state
Bewinxed Jun 2, 2026
f91ac4d
chore: normalize SCIM audit log provider and action fields
Bewinxed Jun 2, 2026
1578e13
fix: lock user row during SCIM reactivation to prevent race
Bewinxed Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (
github.com/consensys/gnark-crypto v0.18.1 // indirect
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/di-wu/parser v0.2.2 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
Expand Down Expand Up @@ -96,6 +97,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/lestrrat-go/jwx/v2 v2.1.0
github.com/oapi-codegen/runtime v1.1.1
github.com/scim2/filter-parser/v2 v2.2.0
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20240303152453-e0e82adf1721
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869
github.com/xeipuuv/gojsonschema v1.2.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5il
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/di-wu/parser v0.2.2 h1:I9oHJ8spBXOeL7Wps0ffkFFFiXJf/pk7NX9lcAMqRMU=
github.com/di-wu/parser v0.2.2/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo=
github.com/didip/tollbooth/v5 v5.1.1 h1:QpKFg56jsbNuQ6FFj++Z1gn2fbBsvAc1ZPLUaDOYW5k=
github.com/didip/tollbooth/v5 v5.1.1/go.mod h1:d9rzwOULswrD3YIrAQmP3bfjxab32Df4IaO6+D25l9g=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
Expand Down Expand Up @@ -362,6 +364,8 @@ github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/
github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM=
github.com/scim2/filter-parser/v2 v2.2.0/go.mod h1:jWnkDToqX/Y0ugz0P5VvpVEUKcWcyHHj+X+je9ce5JA=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM=
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
Expand Down
1 change: 1 addition & 0 deletions hack/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha"
GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000"
GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s"
GOTRUE_SAML_ENABLED="true"
GOTRUE_SCIM_ENABLED="true"
GOTRUE_SAML_PRIVATE_KEY="MIIEowIBAAKCAQEAszrVveMQcSsa0Y+zN1ZFb19cRS0jn4UgIHTprW2tVBmO2PABzjY3XFCfx6vPirMAPWBYpsKmXrvm1tr0A6DZYmA8YmJd937VUQ67fa6DMyppBYTjNgGEkEhmKuszvF3MARsIKCGtZqUrmS7UG4404wYxVppnr2EYm3RGtHlkYsXu20MBqSDXP47bQP+PkJqC3BuNGk3xt5UHl2FSFpTHelkI6lBynw16B+lUT1F96SERNDaMqi/TRsZdGe5mB/29ngC/QBMpEbRBLNRir5iUevKS7Pn4aph9Qjaxx/97siktK210FJT23KjHpgcUfjoQ6BgPBTLtEeQdRyDuc/CgfwIDAQABAoIBAGYDWOEpupQPSsZ4mjMnAYJwrp4ZISuMpEqVAORbhspVeb70bLKonT4IDcmiexCg7cQBcLQKGpPVM4CbQ0RFazXZPMVq470ZDeWDEyhoCfk3bGtdxc1Zc9CDxNMs6FeQs6r1beEZug6weG5J/yRn/qYxQife3qEuDMl+lzfl2EN3HYVOSnBmdt50dxRuX26iW3nqqbMRqYn9OHuJ1LvRRfYeyVKqgC5vgt/6Tf7DAJwGe0dD7q08byHV8DBZ0pnMVU0bYpf1GTgMibgjnLjK//EVWafFHtN+RXcjzGmyJrk3+7ZyPUpzpDjO21kpzUQLrpEkkBRnmg6bwHnSrBr8avECgYEA3pq1PTCAOuLQoIm1CWR9/dhkbJQiKTJevlWV8slXQLR50P0WvI2RdFuSxlWmA4xZej8s4e7iD3MYye6SBsQHygOVGc4efvvEZV8/XTlDdyj7iLVGhnEmu2r7AFKzy8cOvXx0QcLg+zNd7vxZv/8D3Qj9Jje2LjLHKM5n/dZ3RzUCgYEAzh5Lo2anc4WN8faLGt7rPkGQF+7/18ImQE11joHWa3LzAEy7FbeOGpE/vhOv5umq5M/KlWFIRahMEQv4RusieHWI19ZLIP+JwQFxWxS+cPp3xOiGcquSAZnlyVSxZ//dlVgaZq2o2MfrxECcovRlaknl2csyf+HjFFwKlNxHm2MCgYAr//R3BdEy0oZeVRndo2lr9YvUEmu2LOihQpWDCd0fQw0ZDA2kc28eysL2RROte95r1XTvq6IvX5a0w11FzRWlDpQ4J4/LlcQ6LVt+98SoFwew+/PWuyLmxLycUbyMOOpm9eSc4wJJZNvaUzMCSkvfMtmm5jgyZYMMQ9A2Ul/9SQKBgB9mfh9mhBwVPIqgBJETZMMXOdxrjI5SBYHGSyJqpT+5Q0vIZLfqPrvNZOiQFzwWXPJ+tV4Mc/YorW3rZOdo6tdvEGnRO6DLTTEaByrY/io3/gcBZXoSqSuVRmxleqFdWWRnB56c1hwwWLqNHU+1671FhL6pNghFYVK4suP6qu4BAoGBAMk+VipXcIlD67mfGrET/xDqiWWBZtgTzTMjTpODhDY1GZck1eb4CQMP5j5V3gFJ4cSgWDJvnWg8rcz0unz/q4aeMGl1rah5WNDWj1QKWMS6vJhMHM/rqN1WHWR0ZnV83svYgtg0zDnQKlLujqW4JmGXLMU7ur6a+e6lpa1fvLsP"
GOTRUE_MAX_VERIFIED_FACTORS=10
GOTRUE_SMS_TEST_OTP_VALID_UNTIL=""
Expand Down
4 changes: 2 additions & 2 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error {
}

if banDuration != nil {
if terr := user.Ban(tx, *banDuration); terr != nil {
if terr := user.Ban(tx, *banDuration, nil); terr != nil {
return terr
}
}
Expand Down Expand Up @@ -493,7 +493,7 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error {
}

if banDuration != nil {
if terr := user.Ban(tx, *banDuration); terr != nil {
if terr := user.Ban(tx, *banDuration, nil); terr != nil {
return terr
}
}
Expand Down
52 changes: 52 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,48 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
r.Post("/", api.ExternalProviderCallback)
})

// SCIM v2 API endpoints
if api.config.SCIM.Enabled {
r.Route("/scim/v2", func(r *router) {
r.Use(api.requireSCIMAuthentication)

// SCIM-specific NotFound handler for proper error format
r.NotFound(api.scimNotFound)
r.MethodNotAllowed(api.scimMethodNotAllowed)

// Service Provider Configuration
r.Get("/ServiceProviderConfig", api.scimServiceProviderConfig)
r.Get("/ResourceTypes", api.scimResourceTypes)
r.Get("/ResourceTypes/{resource_type_id}", api.scimResourceTypeByID)
r.Get("/Schemas", api.scimSchemas)
r.Get("/Schemas/{schema_id}", api.scimSchemaByID)

// User endpoints
r.Route("/Users", func(r *router) {
r.Get("/", api.scimListUsers)
r.Post("/", api.scimCreateUser)
r.Route("/{user_id}", func(r *router) {
r.Get("/", api.scimGetUser)
r.Put("/", api.scimReplaceUser)
r.Patch("/", api.scimPatchUser)
r.Delete("/", api.scimDeleteUser)
})
})

// Group endpoints
r.Route("/Groups", func(r *router) {
r.Get("/", api.scimListGroups)
r.Post("/", api.scimCreateGroup)
r.Route("/{group_id}", func(r *router) {
r.Get("/", api.scimGetGroup)
r.Put("/", api.scimReplaceGroup)
r.Patch("/", api.scimPatchGroup)
r.Delete("/", api.scimDeleteGroup)
})
})
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

r.Route("/", func(r *router) {

r.Use(api.isValidExternalHost)
Expand Down Expand Up @@ -390,6 +432,16 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
r.Get("/", api.adminSSOProvidersGet)
r.Put("/", api.adminSSOProvidersUpdate)
r.Delete("/", api.adminSSOProvidersDelete)

// SCIM management endpoints
if api.config.SCIM.Enabled {
r.Route("/scim", func(r *router) {
r.Get("/", api.adminSSOProviderGetSCIM)
r.Post("/", api.adminSSOProviderEnableSCIM)
r.Delete("/", api.adminSSOProviderDisableSCIM)
r.Post("/rotate", api.adminSSOProviderRotateSCIMToken)
})
}
})
})
})
Expand Down
78 changes: 78 additions & 0 deletions internal/api/apierrors/apierrors.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,81 @@ func (e *HTTPError) WithInternalMessage(fmtString string, args ...any) *HTTPErro
e.InternalMessage = fmt.Sprintf(fmtString, args...)
return e
}

// SCIMHTTPError is an error with SCIM-specific format per RFC 7644 Section 3.12
type SCIMHTTPError struct {
HTTPStatus int `json:"-"`
Schemas []string `json:"schemas"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
ScimType string `json:"scimType,omitempty"`
InternalError error `json:"-"`
InternalMessage string `json:"-"`
}

const SCIMSchemaError = "urn:ietf:params:scim:api:messages:2.0:Error"

func NewSCIMHTTPError(httpStatus int, detail string, scimType string) *SCIMHTTPError {
return &SCIMHTTPError{
HTTPStatus: httpStatus,
Schemas: []string{SCIMSchemaError},
Status: fmt.Sprintf("%d", httpStatus),
Detail: detail,
ScimType: scimType,
}
}

func NewSCIMBadRequestError(detail string, scimType string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusBadRequest, detail, scimType)
}

func NewSCIMNotFoundError(detail string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusNotFound, detail, "")
}

func NewSCIMUnauthorizedError(detail string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusUnauthorized, detail, "")
}

func NewSCIMConflictError(detail string, scimType string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusConflict, detail, scimType)
}

func NewSCIMForbiddenError(detail string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusForbidden, detail, "")
}

func NewSCIMRequestTooLargeError(detail string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusRequestEntityTooLarge, detail, "")
}

func NewSCIMInternalServerError(detail string) *SCIMHTTPError {
return NewSCIMHTTPError(http.StatusInternalServerError, detail, "")
}

func (e *SCIMHTTPError) Error() string {
if e.InternalMessage != "" {
return e.InternalMessage
}
return fmt.Sprintf("%d: %s", e.HTTPStatus, e.Detail)
}

// Cause returns the root cause error
func (e *SCIMHTTPError) Cause() error {
if e.InternalError != nil {
return e.InternalError
}
return e
}

// WithInternalError adds internal error information to the error
func (e *SCIMHTTPError) WithInternalError(err error) *SCIMHTTPError {
e.InternalError = err
return e
}

// WithInternalMessage adds internal message information to the error
func (e *SCIMHTTPError) WithInternalMessage(fmtString string, args ...any) *SCIMHTTPError {
e.InternalMessage = fmt.Sprintf(fmtString, args...)
return e
}
2 changes: 2 additions & 0 deletions internal/api/apierrors/errorcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
ErrorCodeOAuthInvalidState ErrorCode = "oauth_invalid_state"
ErrorCodeSignupDisabled ErrorCode = "signup_disabled"
ErrorCodeUserBanned ErrorCode = "user_banned"
ErrorCodeUserLocked ErrorCode = "user_locked"
ErrorCodeProviderEmailNeedsVerification ErrorCode = "provider_email_needs_verification"
ErrorCodeInviteNotFound ErrorCode = "invite_not_found"
ErrorCodeBadOAuthState ErrorCode = "bad_oauth_state"
Expand Down Expand Up @@ -62,6 +63,7 @@ const (
ErrorCodeUserAlreadyExists ErrorCode = "user_already_exists"
ErrorCodeSSOProviderNotFound ErrorCode = "sso_provider_not_found"
ErrorCodeSSOProviderDisabled ErrorCode = "sso_provider_disabled"
ErrorCodeSCIMDisabled ErrorCode = "scim_disabled"
ErrorCodeSAMLMetadataFetchFailed ErrorCode = "saml_metadata_fetch_failed"
ErrorCodeSAMLIdPAlreadyExists ErrorCode = "saml_idp_already_exists"
ErrorCodeSSODomainAlreadyExists ErrorCode = "sso_domain_already_exists"
Expand Down
13 changes: 13 additions & 0 deletions internal/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ func HandleResponseError(err error, w http.ResponseWriter, r *http.Request) {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}

case *apierrors.SCIMHTTPError:
switch {
case e.HTTPStatus >= http.StatusInternalServerError:
log.WithError(e.Cause()).Error(e.Error())
case e.HTTPStatus == http.StatusTooManyRequests:
log.WithError(e.Cause()).Warn(e.Error())
default:
log.WithError(e.Cause()).Info(e.Error())
}
if jsonErr := sendSCIMJSON(w, e.HTTPStatus, e); jsonErr != nil && jsonErr != context.DeadlineExceeded {
log.WithError(jsonErr).Warn("Failed to send JSON on ResponseWriter")
}

case ErrorCause:
HandleResponseError(e.Cause(), w, r)

Expand Down
4 changes: 4 additions & 0 deletions internal/api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
return 0, nil, apierrors.NewForbiddenError(apierrors.ErrorCodeUserBanned, "User is banned")
}

if user.IsLocked() {
return 0, nil, apierrors.NewForbiddenError(apierrors.ErrorCodeUserLocked, "User account is locked")
}

hasEmails := providerType != "web3" && !(emailOptional && decision.CandidateEmail.Email == "")

if hasEmails && !user.IsConfirmed() {
Expand Down
4 changes: 4 additions & 0 deletions internal/api/oauthserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ func (s *Server) handleAuthorizationCodeGrant(ctx context.Context, w http.Respon
return apierrors.NewOAuthError("access_denied", "User is banned")
}

if user.IsLocked() {
return apierrors.NewOAuthError("access_denied", "User account is locked")
}

// Exchange the authorization code for tokens
var tokenResponse *tokens.AccessTokenResponse
var grantParams models.GrantParams
Expand Down
8 changes: 8 additions & 0 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ func (r *router) UseBypass(fn func(next http.Handler) http.Handler) {
r.chi.Use(fn)
}

func (r *router) NotFound(fn apiHandler) {
r.chi.NotFound(handler(fn))
}

func (r *router) MethodNotAllowed(fn apiHandler) {
r.chi.MethodNotAllowed(handler(fn))
}

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.chi.ServeHTTP(w, req)
}
Expand Down
Loading
Loading