better_auth-scim 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +32 -3
- data/lib/better_auth/plugins/scim.rb +2 -1
- data/lib/better_auth/scim/mappings.rb +3 -2
- data/lib/better_auth/scim/middlewares.rb +1 -0
- data/lib/better_auth/scim/provider_management.rb +3 -1
- data/lib/better_auth/scim/routes.rb +52 -12
- data/lib/better_auth/scim/scim_filters.rb +1 -1
- data/lib/better_auth/scim/scim_metadata.rb +42 -0
- data/lib/better_auth/scim/scim_tokens.rb +1 -1
- data/lib/better_auth/scim/validation.rb +20 -7
- data/lib/better_auth/scim/version.rb +1 -1
- metadata +20 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ffe12400df4278b53ac2c8673635a6db362e875fb85c3d268e079a97b2a0568
|
|
4
|
+
data.tar.gz: d52b222ff8315e95df2df47922acc44e4eaa625a9df903d472a55d63c9130e90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf07cda2658e52e8f3bc61164298df2b785f81ed1227e66ae2e03c9217e9d426fddc8f4d487c68f8212d6995d5f33a82efbf4f96a973116ddc4cd518bf5e7996
|
|
7
|
+
data.tar.gz: 4736ac7244374c48111234fb9b210b6b54e5330bc3f1f1063e34e2397cd08c544f03f8f33f5084b48a77fe6800030ba27d6b976568adca8be1f63678991de44f
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.0 - 2026-05-21
|
|
6
|
+
|
|
7
|
+
- Improved SCIM route, adapter, user, and rate-limit coverage.
|
|
8
|
+
- Clarified SCIM setup docs and runtime dependencies.
|
|
9
|
+
|
|
5
10
|
## 0.7.0 - 2026-05-05
|
|
6
11
|
|
|
7
12
|
- Changed generated SCIM provider tokens to use hashed storage by default. Set `store_scim_token: "plain"` only when plaintext database storage is intentionally required.
|
data/README.md
CHANGED
|
@@ -14,9 +14,7 @@ require "better_auth/scim"
|
|
|
14
14
|
|
|
15
15
|
BetterAuth.auth(
|
|
16
16
|
plugins: [
|
|
17
|
-
BetterAuth::Plugins.scim
|
|
18
|
-
provider_ownership: { enabled: true }
|
|
19
|
-
)
|
|
17
|
+
BetterAuth::Plugins.scim
|
|
20
18
|
]
|
|
21
19
|
)
|
|
22
20
|
```
|
|
@@ -42,6 +40,17 @@ Implemented API methods include token generation, provider connection management
|
|
|
42
40
|
Options use Ruby snake_case names: `store_scim_token`, `default_scim`, `provider_ownership`, `required_role`, `before_scim_token_generated`, and `after_scim_token_generated`.
|
|
43
41
|
`store_scim_token` defaults to `"hashed"` so generated SCIM provider tokens are
|
|
44
42
|
not stored in plaintext.
|
|
43
|
+
`provider_ownership` defaults to `{ enabled: true }`, a Ruby-specific security
|
|
44
|
+
default that scopes personal SCIM provider management to the user who generated
|
|
45
|
+
the provider token. To preserve legacy shared management of non-organization
|
|
46
|
+
providers, configure `provider_ownership: { enabled: false }`; only use that mode
|
|
47
|
+
when all authenticated users who can access SCIM management routes are trusted to
|
|
48
|
+
view, rotate, or delete unowned personal providers.
|
|
49
|
+
|
|
50
|
+
Organization-scoped SCIM bearer tokens must include the organization component
|
|
51
|
+
generated by `generate_scim_token`. Removing that component invalidates the token.
|
|
52
|
+
Deleting a SCIM user unlinks the current provider account first; the underlying
|
|
53
|
+
Better Auth user is deleted only when no other accounts remain.
|
|
45
54
|
|
|
46
55
|
The plugin exposes upstream-style surface metadata:
|
|
47
56
|
|
|
@@ -52,3 +61,23 @@ The plugin exposes upstream-style surface metadata:
|
|
|
52
61
|
## Production recommendations
|
|
53
62
|
|
|
54
63
|
- In the accounts table (`accounts` or the configured table name), use a unique composite index on `(providerId, accountId)` to prevent duplicate SCIM accounts under concurrent provisioning. The gem does not create this constraint automatically because index syntax and migrations depend on your database adapter and application.
|
|
64
|
+
|
|
65
|
+
## Testing
|
|
66
|
+
|
|
67
|
+
Run the default SCIM package suite from this package directory:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
rbenv exec bundle exec rake test
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The suite includes SCIM route coverage for memory, custom, secondary-storage,
|
|
74
|
+
and database-backed rate limiting. It also runs a live SQLite adapter smoke test
|
|
75
|
+
against a temporary database.
|
|
76
|
+
|
|
77
|
+
External adapter smoke tests are skip-gated. They run only when the relevant gem
|
|
78
|
+
and service are available:
|
|
79
|
+
|
|
80
|
+
- PostgreSQL: `BETTER_AUTH_POSTGRES_URL`
|
|
81
|
+
- MySQL: `BETTER_AUTH_MYSQL_HOST`, `BETTER_AUTH_MYSQL_PORT`, `BETTER_AUTH_MYSQL_USER`, `BETTER_AUTH_MYSQL_PASSWORD`, `BETTER_AUTH_MYSQL_DATABASE`
|
|
82
|
+
- MSSQL: `BETTER_AUTH_MSSQL_URL`, `BETTER_AUTH_MSSQL_MASTER_URL`
|
|
83
|
+
- MongoDB: `BETTER_AUTH_MONGODB_URL`
|
|
@@ -24,7 +24,7 @@ module BetterAuth
|
|
|
24
24
|
singleton_class.remove_method(:scim) if singleton_class.method_defined?(:scim) || singleton_class.private_method_defined?(:scim)
|
|
25
25
|
|
|
26
26
|
def scim(options = {})
|
|
27
|
-
config = {store_scim_token: "hashed"}.merge(normalize_hash(options))
|
|
27
|
+
config = {store_scim_token: "hashed", provider_ownership: {enabled: true}}.merge(normalize_hash(options))
|
|
28
28
|
Plugin.new(
|
|
29
29
|
id: "scim",
|
|
30
30
|
version: BetterAuth::SCIM::VERSION,
|
|
@@ -61,6 +61,7 @@ module BetterAuth
|
|
|
61
61
|
|
|
62
62
|
{
|
|
63
63
|
scimProvider: {
|
|
64
|
+
model_name: "scim_providers",
|
|
64
65
|
fields: scim_provider_fields
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -5,9 +5,10 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def scim_user_update(body)
|
|
8
|
+
email = scim_primary_email(body)&.downcase
|
|
8
9
|
{
|
|
9
|
-
email:
|
|
10
|
-
name: scim_display_name(body,
|
|
10
|
+
email: email,
|
|
11
|
+
name: scim_display_name(body, email),
|
|
11
12
|
updatedAt: Time.now
|
|
12
13
|
}.compact
|
|
13
14
|
end
|
|
@@ -23,6 +23,7 @@ module BetterAuth
|
|
|
23
23
|
where: [{field: "providerId", value: provider_id}].tap { |where| where << {field: "organizationId", value: organization_id} if organization_id }
|
|
24
24
|
)
|
|
25
25
|
raise scim_error("UNAUTHORIZED", "Invalid SCIM token") unless provider
|
|
26
|
+
raise scim_error("UNAUTHORIZED", "Invalid SCIM token") unless provider["organizationId"].to_s == organization_id.to_s
|
|
26
27
|
raise scim_error("UNAUTHORIZED", "Invalid SCIM token") unless scim_token_matches?(ctx, config, token, provider.fetch("scimToken"))
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -49,7 +49,7 @@ module BetterAuth
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def scim_assert_provider_access!(ctx, user_id, provider, required_roles)
|
|
52
|
+
def scim_assert_provider_access!(ctx, user_id, provider, required_roles, config = {})
|
|
53
53
|
return unless provider
|
|
54
54
|
|
|
55
55
|
organization_id = provider["organizationId"]
|
|
@@ -59,6 +59,8 @@ module BetterAuth
|
|
|
59
59
|
member = scim_find_organization_member(ctx, user_id, organization_id)
|
|
60
60
|
raise APIError.new("FORBIDDEN", message: "You must be a member of the organization to access this provider") unless member
|
|
61
61
|
raise APIError.new("FORBIDDEN", message: "Insufficient role for this operation") unless scim_has_required_role?(member.fetch("role", ""), required_roles)
|
|
62
|
+
elsif scim_provider_ownership_enabled?(config)
|
|
63
|
+
raise APIError.new("FORBIDDEN", message: "You must be the owner to access this provider") unless provider["userId"] == user_id
|
|
62
64
|
elsif provider.key?("userId") && provider["userId"] && provider["userId"] != user_id
|
|
63
65
|
raise APIError.new("FORBIDDEN", message: "You must be the owner to access this provider")
|
|
64
66
|
end
|
|
@@ -7,7 +7,7 @@ module BetterAuth
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def scim_generate_token_endpoint(config)
|
|
10
|
-
Endpoint.new(path: "/scim/generate-token", method: "POST", metadata:
|
|
10
|
+
Endpoint.new(path: "/scim/generate-token", method: "POST", metadata: scim_generate_token_openapi_metadata) do |ctx|
|
|
11
11
|
session = Routes.current_session(ctx)
|
|
12
12
|
body = normalize_hash(ctx.body)
|
|
13
13
|
raw_provider_id = body[:provider_id]
|
|
@@ -34,7 +34,7 @@ module BetterAuth
|
|
|
34
34
|
|
|
35
35
|
existing = ctx.context.adapter.find_one(model: "scimProvider", where: [{field: "providerId", value: provider_id}])
|
|
36
36
|
if existing
|
|
37
|
-
scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), existing, required_roles)
|
|
37
|
+
scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), existing, required_roles, config)
|
|
38
38
|
ctx.context.adapter.delete(model: "scimProvider", where: [{field: "id", value: existing.fetch("id")}])
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -61,6 +61,8 @@ module BetterAuth
|
|
|
61
61
|
if organization_id
|
|
62
62
|
member = org_memberships[organization_id]
|
|
63
63
|
member && scim_has_required_role?(member.fetch("role", ""), required_roles)
|
|
64
|
+
elsif scim_provider_ownership_enabled?(config)
|
|
65
|
+
provider["userId"] == user_id
|
|
64
66
|
else
|
|
65
67
|
!provider.key?("userId") || provider["userId"].nil? || provider["userId"] == user_id
|
|
66
68
|
end
|
|
@@ -73,17 +75,17 @@ module BetterAuth
|
|
|
73
75
|
Endpoint.new(path: "/scim/get-provider-connection", method: "GET", metadata: scim_openapi_metadata("Get SCIM provider connection.")) do |ctx|
|
|
74
76
|
session = Routes.current_session(ctx)
|
|
75
77
|
provider = scim_provider_by_provider_id!(ctx, scim_provider_id_query(ctx))
|
|
76
|
-
scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config))
|
|
78
|
+
scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config), config)
|
|
77
79
|
ctx.json(scim_normalized_provider(provider))
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def scim_delete_provider_connection_endpoint(config)
|
|
82
|
-
Endpoint.new(path: "/scim/delete-provider-connection", method: "POST", metadata:
|
|
84
|
+
Endpoint.new(path: "/scim/delete-provider-connection", method: "POST", metadata: scim_delete_provider_openapi_metadata) do |ctx|
|
|
83
85
|
session = Routes.current_session(ctx)
|
|
84
86
|
body = normalize_hash(ctx.body)
|
|
85
87
|
provider = scim_provider_by_provider_id!(ctx, body[:provider_id])
|
|
86
|
-
scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config))
|
|
88
|
+
scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config), config)
|
|
87
89
|
ctx.context.adapter.delete(model: "scimProvider", where: [{field: "providerId", value: provider.fetch("providerId")}])
|
|
88
90
|
ctx.json({success: true})
|
|
89
91
|
end
|
|
@@ -168,8 +170,12 @@ module BetterAuth
|
|
|
168
170
|
|
|
169
171
|
def scim_delete_user_endpoint(config)
|
|
170
172
|
Endpoint.new(path: "/scim/v2/Users/:userId", method: "DELETE", metadata: scim_hidden_metadata("Delete SCIM user.", [*SCIM_SUPPORTED_MEDIA_TYPES, ""]), use: [scim_auth_middleware(config)]) do |ctx|
|
|
171
|
-
user, = scim_find_user_with_account!(ctx)
|
|
172
|
-
ctx.context.
|
|
173
|
+
user, account = scim_find_user_with_account!(ctx)
|
|
174
|
+
ctx.context.adapter.transaction do
|
|
175
|
+
ctx.context.internal_adapter.delete_account(account.fetch("id"))
|
|
176
|
+
remaining_accounts = ctx.context.internal_adapter.find_accounts(user.fetch("id"))
|
|
177
|
+
ctx.context.internal_adapter.delete_user(user.fetch("id")) if remaining_accounts.empty?
|
|
178
|
+
end
|
|
173
179
|
ctx.json(nil, status: 204)
|
|
174
180
|
end
|
|
175
181
|
end
|
|
@@ -177,9 +183,14 @@ module BetterAuth
|
|
|
177
183
|
def scim_list_users_endpoint(config)
|
|
178
184
|
Endpoint.new(path: "/scim/v2/Users", method: "GET", metadata: scim_hidden_metadata("List SCIM users.", SCIM_SUPPORTED_MEDIA_TYPES), use: [scim_auth_middleware(config)]) do |ctx|
|
|
179
185
|
provider = ctx.context.scim_provider
|
|
186
|
+
filter_value = nil
|
|
187
|
+
if ctx.query[:filter] || ctx.query["filter"]
|
|
188
|
+
_filter_field, filter_value = scim_parse_filter(ctx.query[:filter] || ctx.query["filter"])
|
|
189
|
+
end
|
|
190
|
+
start_index, count = scim_list_pagination(ctx.query)
|
|
191
|
+
empty_list = {schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: 0, itemsPerPage: 0, startIndex: start_index, Resources: []}
|
|
180
192
|
accounts = ctx.context.adapter.find_many(model: "account", where: [{field: "providerId", value: provider.fetch("providerId")}])
|
|
181
193
|
account_user_ids = accounts.map { |account| account.fetch("userId") }.uniq
|
|
182
|
-
empty_list = {schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: 0, itemsPerPage: 0, startIndex: 1, Resources: []}
|
|
183
194
|
if account_user_ids.empty?
|
|
184
195
|
ctx.json(empty_list)
|
|
185
196
|
else
|
|
@@ -198,15 +209,22 @@ module BetterAuth
|
|
|
198
209
|
ctx.json(empty_list)
|
|
199
210
|
else
|
|
200
211
|
where = [{field: "id", value: user_ids, operator: "in"}]
|
|
201
|
-
if
|
|
202
|
-
_filter_field, filter_value = scim_parse_filter(ctx.query[:filter] || ctx.query["filter"])
|
|
212
|
+
if filter_value
|
|
203
213
|
where << {field: "email", value: filter_value.to_s.downcase, operator: "eq"}
|
|
204
214
|
end
|
|
205
215
|
|
|
206
|
-
|
|
216
|
+
total_results = ctx.context.internal_adapter.count_total_users(where: where)
|
|
217
|
+
offset = start_index - 1
|
|
218
|
+
limit = count || ((offset > 0) ? [total_results - offset, 0].max : nil)
|
|
219
|
+
users = ctx.context.internal_adapter.list_users(
|
|
220
|
+
where: where,
|
|
221
|
+
sort_by: {field: "email", direction: "asc"},
|
|
222
|
+
limit: limit,
|
|
223
|
+
offset: offset.positive? ? offset : nil
|
|
224
|
+
)
|
|
207
225
|
accounts_by_user = accounts.each_with_object({}) { |account, result| result[account.fetch("userId")] ||= account }
|
|
208
226
|
resources = users.map { |user| scim_user_resource(user, accounts_by_user[user.fetch("id")], ctx.context.base_url) }
|
|
209
|
-
ctx.json({schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults:
|
|
227
|
+
ctx.json({schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: total_results, itemsPerPage: resources.length, startIndex: start_index, Resources: resources})
|
|
210
228
|
end
|
|
211
229
|
end
|
|
212
230
|
end
|
|
@@ -293,5 +311,27 @@ module BetterAuth
|
|
|
293
311
|
|
|
294
312
|
[user, account]
|
|
295
313
|
end
|
|
314
|
+
|
|
315
|
+
def scim_list_pagination(query)
|
|
316
|
+
start_index = scim_positive_integer(query[:startIndex] || query[:start_index] || query["startIndex"] || query["start_index"], 1)
|
|
317
|
+
count = if query.key?(:count) || query.key?("count")
|
|
318
|
+
scim_non_negative_integer(query[:count] || query["count"], 0)
|
|
319
|
+
end
|
|
320
|
+
[start_index, count]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def scim_positive_integer(value, fallback)
|
|
324
|
+
parsed = Integer(value)
|
|
325
|
+
parsed.positive? ? parsed : fallback
|
|
326
|
+
rescue ArgumentError, TypeError
|
|
327
|
+
fallback
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def scim_non_negative_integer(value, fallback)
|
|
331
|
+
parsed = Integer(value)
|
|
332
|
+
parsed.negative? ? fallback : parsed
|
|
333
|
+
rescue ArgumentError, TypeError
|
|
334
|
+
fallback
|
|
335
|
+
end
|
|
296
336
|
end
|
|
297
337
|
end
|
|
@@ -6,7 +6,7 @@ module BetterAuth
|
|
|
6
6
|
|
|
7
7
|
def scim_parse_filter(filter)
|
|
8
8
|
match = filter.to_s.match(/\A\s*([^\s]+)\s+(eq|ne|co|sw|ew|pr)\s*(?:"([^"]*)"|([^\s]+))?\s*\z/i)
|
|
9
|
-
raise scim_error("BAD_REQUEST", "Invalid
|
|
9
|
+
raise scim_error("BAD_REQUEST", "Invalid filter expression", scim_type: "invalidFilter") unless match
|
|
10
10
|
|
|
11
11
|
field = match[1]
|
|
12
12
|
operator = match[2].downcase
|
|
@@ -29,6 +29,48 @@ module BetterAuth
|
|
|
29
29
|
}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def scim_generate_token_openapi_metadata
|
|
33
|
+
{
|
|
34
|
+
openapi: {
|
|
35
|
+
summary: "Generates a new SCIM token for the given provider",
|
|
36
|
+
requestBody: OpenAPI.json_request_body(
|
|
37
|
+
OpenAPI.object_schema(
|
|
38
|
+
{
|
|
39
|
+
provider_id: {type: "string", description: "SCIM provider identifier"},
|
|
40
|
+
organization_id: {type: "string", description: "Organization ID to restrict the SCIM token to"}
|
|
41
|
+
},
|
|
42
|
+
required: ["provider_id"]
|
|
43
|
+
)
|
|
44
|
+
),
|
|
45
|
+
responses: scim_openapi_responses.merge(
|
|
46
|
+
"201" => OpenAPI.json_response(
|
|
47
|
+
"SCIM token generated",
|
|
48
|
+
OpenAPI.object_schema({scimToken: {type: "string"}}, required: ["scimToken"])
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def scim_delete_provider_openapi_metadata
|
|
56
|
+
{
|
|
57
|
+
openapi: {
|
|
58
|
+
summary: "Delete SCIM provider connection.",
|
|
59
|
+
requestBody: OpenAPI.json_request_body(
|
|
60
|
+
OpenAPI.object_schema(
|
|
61
|
+
{
|
|
62
|
+
provider_id: {type: "string", description: "SCIM provider identifier"}
|
|
63
|
+
},
|
|
64
|
+
required: ["provider_id"]
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
responses: scim_openapi_responses.merge(
|
|
68
|
+
"200" => OpenAPI.json_response("SCIM provider connection deleted", OpenAPI.success_response_schema)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
32
74
|
def scim_openapi_responses
|
|
33
75
|
{
|
|
34
76
|
"200" => {description: "Success"},
|
|
@@ -48,7 +48,7 @@ module BetterAuth
|
|
|
48
48
|
def scim_default_provider(config, provider_id, organization_id)
|
|
49
49
|
Array(config[:default_scim]).find do |provider|
|
|
50
50
|
candidate = normalize_hash(provider)
|
|
51
|
-
next true if candidate[:provider_id].to_s == provider_id.to_s && organization_id.to_s.empty?
|
|
51
|
+
next true if candidate[:provider_id].to_s == provider_id.to_s && organization_id.to_s.empty? && candidate[:organization_id].to_s.empty?
|
|
52
52
|
|
|
53
53
|
candidate[:provider_id].to_s == provider_id.to_s &&
|
|
54
54
|
!organization_id.to_s.empty? &&
|
|
@@ -4,6 +4,9 @@ module BetterAuth
|
|
|
4
4
|
module Plugins
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
SCIM_MAX_PATCH_OPERATIONS = 100
|
|
8
|
+
SCIM_MAX_PATCH_VALUE_DEPTH = 5
|
|
9
|
+
|
|
7
10
|
def scim_validate_user_body!(body)
|
|
8
11
|
raise scim_error("BAD_REQUEST", BASE_ERROR_CODES["VALIDATION_ERROR"]) unless body[:user_name].is_a?(String)
|
|
9
12
|
raise scim_error("BAD_REQUEST", BASE_ERROR_CODES["VALIDATION_ERROR"]) if body[:user_name].empty?
|
|
@@ -26,8 +29,13 @@ module BetterAuth
|
|
|
26
29
|
schemas = Array(body[:schemas])
|
|
27
30
|
raise scim_error("BAD_REQUEST", "Invalid schemas for PatchOp") unless schemas.include?("urn:ietf:params:scim:api:messages:2.0:PatchOp")
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
operations = body[:operations]
|
|
33
|
+
raise scim_error("BAD_REQUEST", BASE_ERROR_CODES["VALIDATION_ERROR"]) unless operations.is_a?(Array)
|
|
34
|
+
raise scim_error("BAD_REQUEST", "Too many SCIM patch operations") if operations.length > SCIM_MAX_PATCH_OPERATIONS
|
|
35
|
+
|
|
36
|
+
operations.each_with_index do |operation, index|
|
|
37
|
+
normalized = normalize_hash(operation)
|
|
38
|
+
op = normalized[:op]
|
|
31
39
|
next if op.nil? || op.to_s.empty?
|
|
32
40
|
|
|
33
41
|
unless op.is_a?(String)
|
|
@@ -38,14 +46,19 @@ module BetterAuth
|
|
|
38
46
|
|
|
39
47
|
raise scim_patch_validation_error("[body.Operations.#{index}.op] Invalid option: expected one of \"replace\"|\"add\"|\"remove\"")
|
|
40
48
|
end
|
|
49
|
+
|
|
50
|
+
operations.each { |operation| scim_validate_patch_value_depth!(normalize_hash(operation)[:value]) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def scim_validate_patch_value_depth!(value, depth = 0)
|
|
54
|
+
return unless value.is_a?(Hash)
|
|
55
|
+
raise scim_error("BAD_REQUEST", "SCIM patch value is too deeply nested") if depth > SCIM_MAX_PATCH_VALUE_DEPTH
|
|
56
|
+
|
|
57
|
+
value.each_value { |nested| scim_validate_patch_value_depth!(nested, depth + 1) }
|
|
41
58
|
end
|
|
42
59
|
|
|
43
60
|
def scim_patch_validation_error(message)
|
|
44
|
-
|
|
45
|
-
"BAD_REQUEST",
|
|
46
|
-
message: BASE_ERROR_CODES["VALIDATION_ERROR"],
|
|
47
|
-
body: {code: "VALIDATION_ERROR", message: message}
|
|
48
|
-
)
|
|
61
|
+
scim_error("BAD_REQUEST", message)
|
|
49
62
|
end
|
|
50
63
|
end
|
|
51
64
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_auth-scim
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -85,6 +85,20 @@ dependencies:
|
|
|
85
85
|
- - "~>"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '13.2'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: sqlite3
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '2.0'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '2.0'
|
|
88
102
|
- !ruby/object:Gem::Dependency
|
|
89
103
|
name: standardrb
|
|
90
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -127,14 +141,14 @@ files:
|
|
|
127
141
|
- lib/better_auth/scim/utils.rb
|
|
128
142
|
- lib/better_auth/scim/validation.rb
|
|
129
143
|
- lib/better_auth/scim/version.rb
|
|
130
|
-
homepage: https://github.com/sebasxsala/better-auth
|
|
144
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
131
145
|
licenses:
|
|
132
146
|
- MIT
|
|
133
147
|
metadata:
|
|
134
|
-
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
135
|
-
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
136
|
-
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-scim/CHANGELOG.md
|
|
137
|
-
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
148
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
149
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
150
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-scim/CHANGELOG.md
|
|
151
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
138
152
|
rdoc_options: []
|
|
139
153
|
require_paths:
|
|
140
154
|
- lib
|