strongmind-platform-sdk 3.32.0 → 3.33.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 +7 -0
- data/Gemfile.lock +20 -11
- data/lib/platform_sdk/canvas_api/client.rb +8 -1
- data/lib/platform_sdk/identity/error_handleable.rb +60 -5
- data/lib/platform_sdk/identity/zitadel/auth_client.rb +78 -0
- data/lib/platform_sdk/identity/zitadel/client.rb +83 -0
- data/lib/platform_sdk/identity/zitadel/user_payload.rb +67 -0
- data/lib/platform_sdk/identity.rb +11 -7
- data/lib/platform_sdk/version.rb +2 -2
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b23cd42819001403fdf176b48df36e0d0acd9fd65a625e24a65899ba3496715
|
|
4
|
+
data.tar.gz: aa7316e0ef335e0cbc423ae5b9f201b6e076d971373966b60e2db229f9b86fc4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d45af1a0c4478d1310763cf3dd33effe6a96fdc6f76ab1dcbcf15fc710a7ad618d82515ec32d9ed65b4ac520c64d1c6722ba1614c57bf56b77823e003d45641a
|
|
7
|
+
data.tar.gz: 3655fa57a8e56e9e31b8b2aa9c40abca15abe7ed7f35c512a31585992c4cbd985e2aef3884e429c76f6362ca4c83c517389089eb7c86084764abe9f121fe931c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [3.33.0] - 2026-07-28
|
|
4
|
+
|
|
5
|
+
- Add `PlatformSdk::Identity::Zitadel::Client` — Zitadel v2 user API client (`#provision_identity`), mirroring the IS4 client's signature. Preserves the IS4 `Id` as the Zitadel `userId` and treats a `409` as an idempotent replay once the preserved id is confirmed to hold the same username.
|
|
6
|
+
- Add `PlatformSdk::Identity::Zitadel::AuthClient` — fetches and caches machine-user access tokens via `client_credentials`, thread-safe under concurrent callers. `Zitadel::Client` takes any token-provider object responding to `#auth_token` (a seam for wrapping `AuthClient`, e.g. behind `Rails.cache`). `AuthClient` accepts a `scope:` keyword (defaults to the management-API audience) and exposes `#expires_at`.
|
|
7
|
+
- Add `PlatformSdk::Identity::Zitadel::UserPayload.from_is4` — maps the IS4 `WithProfile` payload shape onto Zitadel's `AddHumanUser` request, dropping password/reset-email fields (SIS creations are always shells) and marking email/phone unverified.
|
|
8
|
+
- Redact `client_secret`/bearer-token values from the raised error payload in `ErrorHandleable#raise_error_with_payload`, not just the logged line, since the exception itself is handed to consumer instrumentation.
|
|
9
|
+
|
|
3
10
|
## [3.32.0] - 2026-06-10
|
|
4
11
|
|
|
5
12
|
- Add `PlatformSdk::LlmGateway::Client#generate_offline_activity` — posts to `POST /activities/generate` with `learning_component`, `learning_objective`, and `grade_level` and returns the parsed response body.
|
data/Gemfile.lock
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
strongmind-platform-sdk (3.
|
|
4
|
+
strongmind-platform-sdk (3.33.0)
|
|
5
5
|
asset_sync
|
|
6
6
|
aws-sdk-cloudwatch
|
|
7
7
|
aws-sdk-secretsmanager (~> 1.66)
|
|
8
|
+
base64
|
|
8
9
|
faraday (~> 2.5, >= 2.5.2)
|
|
9
10
|
faraday-retry
|
|
10
11
|
fog-aws
|
|
@@ -124,7 +125,7 @@ GEM
|
|
|
124
125
|
base64 (0.2.0)
|
|
125
126
|
bigdecimal (3.1.8)
|
|
126
127
|
builder (3.3.0)
|
|
127
|
-
cgi (0.5.
|
|
128
|
+
cgi (0.5.2)
|
|
128
129
|
coderay (1.1.3)
|
|
129
130
|
concurrent-ruby (1.3.3)
|
|
130
131
|
connection_pool (2.4.1)
|
|
@@ -138,7 +139,7 @@ GEM
|
|
|
138
139
|
erubi (1.13.0)
|
|
139
140
|
ethon (0.16.0)
|
|
140
141
|
ffi (>= 1.15.0)
|
|
141
|
-
excon (1.
|
|
142
|
+
excon (1.6.0)
|
|
142
143
|
logger
|
|
143
144
|
factory_bot (6.4.6)
|
|
144
145
|
activesupport (>= 5.0.0)
|
|
@@ -152,6 +153,7 @@ GEM
|
|
|
152
153
|
faraday-retry (2.2.1)
|
|
153
154
|
faraday (~> 2.0)
|
|
154
155
|
ffi (1.17.0)
|
|
156
|
+
ffi (1.17.0-arm64-darwin)
|
|
155
157
|
ffi (1.17.0-x86_64-darwin)
|
|
156
158
|
ffi (1.17.0-x86_64-linux-gnu)
|
|
157
159
|
fog-aws (3.33.2)
|
|
@@ -172,11 +174,14 @@ GEM
|
|
|
172
174
|
nokogiri (>= 1.5.11, < 2.0.0)
|
|
173
175
|
formatador (1.2.3)
|
|
174
176
|
reline
|
|
175
|
-
globalid (1.
|
|
177
|
+
globalid (1.4.0)
|
|
176
178
|
activesupport (>= 6.1)
|
|
177
179
|
google-protobuf (4.33.6)
|
|
178
180
|
bigdecimal
|
|
179
181
|
rake (>= 13)
|
|
182
|
+
google-protobuf (4.33.6-arm64-darwin)
|
|
183
|
+
bigdecimal
|
|
184
|
+
rake (>= 13)
|
|
180
185
|
google-protobuf (4.33.6-x86_64-darwin)
|
|
181
186
|
bigdecimal
|
|
182
187
|
rake (>= 13)
|
|
@@ -202,7 +207,7 @@ GEM
|
|
|
202
207
|
loofah (2.22.0)
|
|
203
208
|
crass (~> 1.0.2)
|
|
204
209
|
nokogiri (>= 1.12.0)
|
|
205
|
-
mail (2.9.
|
|
210
|
+
mail (2.9.1)
|
|
206
211
|
logger
|
|
207
212
|
mini_mime (>= 0.1.1)
|
|
208
213
|
net-imap
|
|
@@ -213,7 +218,7 @@ GEM
|
|
|
213
218
|
mime-types (3.7.0)
|
|
214
219
|
logger
|
|
215
220
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
|
216
|
-
mime-types-data (3.2026.
|
|
221
|
+
mime-types-data (3.2026.0701)
|
|
217
222
|
mini_mime (1.1.5)
|
|
218
223
|
mini_portile2 (2.8.7)
|
|
219
224
|
minitest (5.24.1)
|
|
@@ -221,7 +226,7 @@ GEM
|
|
|
221
226
|
mutex_m (0.2.0)
|
|
222
227
|
net-http (0.4.1)
|
|
223
228
|
uri
|
|
224
|
-
net-imap (0.6.
|
|
229
|
+
net-imap (0.6.6)
|
|
225
230
|
date
|
|
226
231
|
net-protocol
|
|
227
232
|
net-pop (0.1.2)
|
|
@@ -231,12 +236,14 @@ GEM
|
|
|
231
236
|
net-smtp (0.5.1)
|
|
232
237
|
net-protocol
|
|
233
238
|
nio4r (2.7.5)
|
|
234
|
-
nokogiri (1.
|
|
239
|
+
nokogiri (1.19.4)
|
|
235
240
|
mini_portile2 (~> 2.8.2)
|
|
236
241
|
racc (~> 1.4)
|
|
237
|
-
nokogiri (1.
|
|
242
|
+
nokogiri (1.19.4-arm64-darwin)
|
|
243
|
+
racc (~> 1.4)
|
|
244
|
+
nokogiri (1.19.4-x86_64-darwin)
|
|
238
245
|
racc (~> 1.4)
|
|
239
|
-
nokogiri (1.
|
|
246
|
+
nokogiri (1.19.4-x86_64-linux-gnu)
|
|
240
247
|
racc (~> 1.4)
|
|
241
248
|
opentelemetry-api (1.8.0)
|
|
242
249
|
logger
|
|
@@ -363,6 +370,7 @@ GEM
|
|
|
363
370
|
redis-client (>= 0.22.2)
|
|
364
371
|
sqlite3 (1.7.3)
|
|
365
372
|
mini_portile2 (~> 2.8.0)
|
|
373
|
+
sqlite3 (1.7.3-arm64-darwin)
|
|
366
374
|
sqlite3 (1.7.3-x86_64-darwin)
|
|
367
375
|
sqlite3 (1.7.3-x86_64-linux)
|
|
368
376
|
stringio (3.1.1)
|
|
@@ -390,13 +398,14 @@ GEM
|
|
|
390
398
|
crack (>= 0.3.2)
|
|
391
399
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
392
400
|
webrick (1.8.1)
|
|
393
|
-
websocket-driver (0.8.
|
|
401
|
+
websocket-driver (0.8.2)
|
|
394
402
|
base64
|
|
395
403
|
websocket-extensions (>= 0.1.0)
|
|
396
404
|
websocket-extensions (0.1.5)
|
|
397
405
|
zeitwerk (2.6.17)
|
|
398
406
|
|
|
399
407
|
PLATFORMS
|
|
408
|
+
arm64-darwin
|
|
400
409
|
ruby
|
|
401
410
|
x86_64-darwin-21
|
|
402
411
|
x86_64-linux
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "faraday"
|
|
4
|
+
require "platform_sdk/version"
|
|
4
5
|
|
|
5
6
|
module PlatformSdk
|
|
6
7
|
module CanvasApiWrapper
|
|
@@ -15,7 +16,13 @@ module PlatformSdk
|
|
|
15
16
|
def initialize(domain:, token:)
|
|
16
17
|
@host = "https://#{domain}"
|
|
17
18
|
@token = token
|
|
18
|
-
@connection = Faraday.new(
|
|
19
|
+
@connection = Faraday.new(
|
|
20
|
+
url: host,
|
|
21
|
+
headers: {
|
|
22
|
+
'Authorization' => "Bearer #{token}",
|
|
23
|
+
'User-Agent' => "StrongMind-PlatformSDK/#{PlatformSdk::VERSION}"
|
|
24
|
+
}
|
|
25
|
+
)
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
# @param course_id [Integer]
|
|
@@ -4,6 +4,13 @@ module PlatformSdk
|
|
|
4
4
|
module Identity
|
|
5
5
|
# Error handling for Identity SDK
|
|
6
6
|
module ErrorHandleable
|
|
7
|
+
# The logged payload is the raw request body, which for token endpoints
|
|
8
|
+
# carries client credentials; redact those fields (form-encoded and
|
|
9
|
+
# JSON shapes) so an auth outage can't write secrets to consumer logs.
|
|
10
|
+
SENSITIVE_KEYS = 'client_secret|client_assertion|refresh_token|password'
|
|
11
|
+
FORM_SECRET_PATTERN = /(#{SENSITIVE_KEYS})=[^&]*/i
|
|
12
|
+
JSON_SECRET_PATTERN = /("(?:#{SENSITIVE_KEYS})"\s*:\s*")[^"]*(")/i
|
|
13
|
+
|
|
7
14
|
def with_rescue
|
|
8
15
|
yield
|
|
9
16
|
rescue Faraday::TimeoutError => e
|
|
@@ -15,14 +22,62 @@ module PlatformSdk
|
|
|
15
22
|
end
|
|
16
23
|
|
|
17
24
|
def raise_error_with_payload(exception_class, error)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
Rails.logger.info(error_log_payload(exception_class, error).to_json) if defined?(Rails)
|
|
26
|
+
# The exception itself carries error.response on to consumer
|
|
27
|
+
# instrumentation (Sentry, custom error handlers), so the raw
|
|
28
|
+
# request body/headers - which can hold a client_secret or bearer
|
|
29
|
+
# token - must be scrubbed here too, not just in the log line above.
|
|
30
|
+
raise exception_class, scrub_response(error.response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# No ActiveSupport here (String#demodulize): this must work for
|
|
34
|
+
# non-Rails consumers, where the log line is skipped but this hash is
|
|
35
|
+
# still built.
|
|
36
|
+
def error_log_payload(exception_class, error)
|
|
37
|
+
{
|
|
38
|
+
exception: exception_class.name.split('::').last,
|
|
39
|
+
payload: scrub_secrets(error.response.dig(:request, :body)),
|
|
21
40
|
response_body: error.response[:body],
|
|
22
41
|
status: error.response[:status]
|
|
23
42
|
}.compact
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scrub_secrets(body)
|
|
46
|
+
return body unless body.is_a?(String)
|
|
47
|
+
|
|
48
|
+
body.gsub(FORM_SECRET_PATTERN) { "#{Regexp.last_match(1)}=[REDACTED]" }
|
|
49
|
+
.gsub(JSON_SECRET_PATTERN, '\1[REDACTED]\2')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns a scrubbed copy of the Faraday error response hash; the
|
|
53
|
+
# original (and its nested :request hash) are left untouched. Only
|
|
54
|
+
# [:request][:body] and [:request][:headers] are touched -
|
|
55
|
+
# [:status] and [:body] (the response body) are read elsewhere
|
|
56
|
+
# (e.g. e.response&.dig(:status), e.response&.dig(:body, 'paramName'))
|
|
57
|
+
# and must reach consumers unchanged.
|
|
58
|
+
def scrub_response(response)
|
|
59
|
+
return response unless response.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
scrubbed = response.dup
|
|
62
|
+
scrubbed[:request] = scrub_request(response[:request]) if response[:request].is_a?(Hash)
|
|
63
|
+
scrubbed
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scrub_request(request)
|
|
67
|
+
scrubbed_request = request.dup
|
|
68
|
+
scrubbed_request[:body] = scrub_secrets(request[:body]) if request[:body].is_a?(String)
|
|
69
|
+
scrubbed_request[:headers] = scrub_headers(request[:headers]) if request[:headers].is_a?(Hash)
|
|
70
|
+
scrubbed_request
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# dup (not a rebuilt Hash) so a Faraday::Utils::Headers keeps its
|
|
74
|
+
# class and case-insensitive lookup on the scrubbed copy.
|
|
75
|
+
def scrub_headers(headers)
|
|
76
|
+
scrubbed = headers.dup
|
|
77
|
+
scrubbed.each_key do |key|
|
|
78
|
+
scrubbed[key] = '[REDACTED]' if key.to_s.casecmp('authorization').zero?
|
|
79
|
+
end
|
|
80
|
+
scrubbed
|
|
26
81
|
end
|
|
27
82
|
end
|
|
28
83
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlatformSdk
|
|
4
|
+
module Identity
|
|
5
|
+
module Zitadel
|
|
6
|
+
# Fetches machine-user access tokens from Zitadel via client_credentials.
|
|
7
|
+
# Tokens are opaque bearer tokens; expiry tracking uses expires_in.
|
|
8
|
+
class AuthClient
|
|
9
|
+
include ErrorHandleable
|
|
10
|
+
|
|
11
|
+
API_SCOPE = 'openid urn:zitadel:iam:org:project:id:zitadel:aud'
|
|
12
|
+
EXPIRY_SKEW_SECONDS = 60
|
|
13
|
+
OPEN_TIMEOUT_SECONDS = 2
|
|
14
|
+
READ_TIMEOUT_SECONDS = 5
|
|
15
|
+
|
|
16
|
+
attr_reader :conn, :expires_at
|
|
17
|
+
|
|
18
|
+
def initialize(base_url, client_id, client_secret, scope: API_SCOPE)
|
|
19
|
+
@client_id = client_id
|
|
20
|
+
@client_secret = client_secret
|
|
21
|
+
@scope = scope
|
|
22
|
+
@token_lock = Mutex.new
|
|
23
|
+
@conn = build_connection(base_url)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The lock makes concurrent callers (Puma/Sidekiq threads) share one
|
|
27
|
+
# fetch instead of racing: an unguarded reader could also observe
|
|
28
|
+
# @token assigned but @expires_at still nil mid-fetch and crash in
|
|
29
|
+
# expired?. Serializing on the bounded (2s/5s) fetch is acceptable
|
|
30
|
+
# for the shadow path.
|
|
31
|
+
def auth_token
|
|
32
|
+
@token_lock.synchronize do
|
|
33
|
+
fetch_token if expired?
|
|
34
|
+
@token
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_connection(base_url)
|
|
41
|
+
Faraday.new(base_url) do |conn|
|
|
42
|
+
conn.request :url_encoded
|
|
43
|
+
conn.response :raise_error
|
|
44
|
+
conn.response :json
|
|
45
|
+
conn.options.open_timeout = OPEN_TIMEOUT_SECONDS
|
|
46
|
+
conn.options.timeout = READ_TIMEOUT_SECONDS
|
|
47
|
+
conn.adapter :net_http
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def expired?
|
|
52
|
+
@token.nil? || Time.now.utc >= @expires_at
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Both fields are read into locals before either ivar is assigned: a
|
|
56
|
+
# malformed 200 (access_token without expires_in) must leave the cache
|
|
57
|
+
# untouched so the next call retries instead of comparing against a
|
|
58
|
+
# nil @expires_at forever.
|
|
59
|
+
def fetch_token
|
|
60
|
+
body = with_rescue { @conn.post('/oauth/v2/token', token_request_params).body }
|
|
61
|
+
token = body.fetch('access_token')
|
|
62
|
+
expires_at = Time.now.utc + body.fetch('expires_in').to_i - EXPIRY_SKEW_SECONDS
|
|
63
|
+
@token = token
|
|
64
|
+
@expires_at = expires_at
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def token_request_params
|
|
68
|
+
{
|
|
69
|
+
grant_type: 'client_credentials',
|
|
70
|
+
client_id: @client_id,
|
|
71
|
+
client_secret: @client_secret,
|
|
72
|
+
scope: @scope
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlatformSdk
|
|
4
|
+
module Identity
|
|
5
|
+
module Zitadel
|
|
6
|
+
# Client for the Zitadel v2 user API. Deliberately mirrors the IS4
|
|
7
|
+
# Client's #provision_identity signature so consuming apps can call
|
|
8
|
+
# either interchangeably. Short timeouts, no retry middleware: this is
|
|
9
|
+
# the best-effort shadow path and must never hang a provisioning request.
|
|
10
|
+
#
|
|
11
|
+
# Token acquisition is a seam: the SDK ships Zitadel::AuthClient as the
|
|
12
|
+
# client_credentials implementation, but this class only needs an
|
|
13
|
+
# object responding to #auth_token. Consumers may pass an AuthClient
|
|
14
|
+
# directly, wrap one (e.g. a Rails.cache-backed provider that shares a
|
|
15
|
+
# token across processes), or supply any other object with that
|
|
16
|
+
# interface.
|
|
17
|
+
class Client
|
|
18
|
+
include ErrorHandleable
|
|
19
|
+
|
|
20
|
+
OPEN_TIMEOUT_SECONDS = 2
|
|
21
|
+
READ_TIMEOUT_SECONDS = 5
|
|
22
|
+
|
|
23
|
+
def initialize(base_url, token_provider)
|
|
24
|
+
@auth = token_provider
|
|
25
|
+
@conn = Faraday.new(base_url) do |conn|
|
|
26
|
+
conn.request :authorization, 'Bearer', -> { @auth.auth_token }
|
|
27
|
+
conn.request :json
|
|
28
|
+
conn.response :raise_error
|
|
29
|
+
conn.response :json
|
|
30
|
+
conn.options.open_timeout = OPEN_TIMEOUT_SECONDS
|
|
31
|
+
conn.options.timeout = READ_TIMEOUT_SECONDS
|
|
32
|
+
conn.adapter :net_http
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def provision_identity(is4_params)
|
|
37
|
+
payload = UserPayload.from_is4(is4_params)
|
|
38
|
+
post_payload('/v2/users/human', payload)
|
|
39
|
+
rescue PlatformSdk::Identity::ClientError => e
|
|
40
|
+
raise unless e.response&.dig(:status) == 409
|
|
41
|
+
|
|
42
|
+
verify_existing!(payload, e)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# A 409 is idempotent-replay success only when the preserved id exists
|
|
48
|
+
# AND still holds this username; a matching id with a different
|
|
49
|
+
# username is a diverged record, not a replay, so the 409 stands.
|
|
50
|
+
def verify_existing!(payload, original_error)
|
|
51
|
+
user = fetch_user(payload[:userId], original_error)
|
|
52
|
+
raise original_error unless user['username'] == payload[:username]
|
|
53
|
+
|
|
54
|
+
{ 'userId' => payload[:userId], 'alreadyExisted' => true }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# An inconclusive verify re-raises the conflict, not the transient: the
|
|
58
|
+
# 409 is the actionable signal (id or username collision) for the
|
|
59
|
+
# caller's hook. Faraday::Error is the ancestor of the SDK taxonomy
|
|
60
|
+
# (ClientError/ServerError/TimeoutError) AND of the transport failures
|
|
61
|
+
# with_rescue never maps (ConnectionFailed, SSLError, ParsingError), so
|
|
62
|
+
# a connect refusal or open timeout is inconclusive too, not a leak.
|
|
63
|
+
def fetch_user(user_id, original_error)
|
|
64
|
+
get_payload("/v2/users/#{user_id}").fetch('user', {})
|
|
65
|
+
rescue Faraday::Error
|
|
66
|
+
raise original_error
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def get_payload(path)
|
|
70
|
+
with_rescue do
|
|
71
|
+
@conn.get(path).body
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def post_payload(path, body)
|
|
76
|
+
with_rescue do
|
|
77
|
+
@conn.post(path) { |req| req.body = body }.body
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlatformSdk
|
|
4
|
+
module Identity
|
|
5
|
+
module Zitadel
|
|
6
|
+
# Maps the IS4 WithProfile payload shape onto Zitadel v2 AddHumanUser.
|
|
7
|
+
# sendPasswordResetEmail / PasswordResetReturnUrl are IS4-only concepts
|
|
8
|
+
# and are dropped. No password is ever sent: SIS creations are shells.
|
|
9
|
+
# The mixed key casing is faithful to the IS4 payload: PascalCase
|
|
10
|
+
# throughout except phoneNumber (see Services::Identity in Central).
|
|
11
|
+
module UserPayload
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def from_is4(is4_params)
|
|
15
|
+
params = is4_params.transform_keys(&:to_s)
|
|
16
|
+
identity_fields(params)
|
|
17
|
+
.merge(profile: profile_fields(params))
|
|
18
|
+
.merge(optional_fields(params))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def identity_fields(params)
|
|
22
|
+
{
|
|
23
|
+
userId: required(params, 'Id'),
|
|
24
|
+
username: required(params, 'Username'),
|
|
25
|
+
email: { email: required(params, 'Email'), isVerified: false }
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def profile_fields(params)
|
|
30
|
+
username = required(params, 'Username')
|
|
31
|
+
{
|
|
32
|
+
givenName: presence(params['FirstName']) || username,
|
|
33
|
+
familyName: presence(params['LastName']) || username
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def optional_fields(params)
|
|
38
|
+
fields = {}
|
|
39
|
+
phone = presence(params['phoneNumber'])
|
|
40
|
+
fields[:phone] = { phone:, isVerified: false } if phone
|
|
41
|
+
fields.merge(metadata_fields(params))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def metadata_fields(params)
|
|
45
|
+
source_system_id = presence(params['SourceSystemId'])
|
|
46
|
+
return {} unless source_system_id
|
|
47
|
+
|
|
48
|
+
{ metadata: [{ key: 'sourceSystemId', value: Base64.strict_encode64(source_system_id) }] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# userId == IS4 Id is the migration's id-preservation invariant; a
|
|
52
|
+
# blank required field must raise here rather than reach Zitadel,
|
|
53
|
+
# which would mint its own id for a null userId.
|
|
54
|
+
def required(params, key)
|
|
55
|
+
presence(params[key]) || raise(ArgumentError, "IS4 payload is missing #{key}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def presence(value)
|
|
59
|
+
stripped = value.to_s.strip
|
|
60
|
+
stripped.empty? ? nil : stripped
|
|
61
|
+
end
|
|
62
|
+
private_class_method :presence, :required, :metadata_fields, :optional_fields,
|
|
63
|
+
:profile_fields, :identity_fields
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'faraday'
|
|
6
|
+
require 'faraday/net_http'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'jwt'
|
|
8
9
|
|
|
9
|
-
require
|
|
10
|
-
require
|
|
10
|
+
require 'platform_sdk/identity/error_handleable'
|
|
11
|
+
require 'platform_sdk/identity/clients'
|
|
12
|
+
require 'platform_sdk/identity/zitadel/auth_client'
|
|
13
|
+
require 'platform_sdk/identity/zitadel/user_payload'
|
|
14
|
+
require 'platform_sdk/identity/zitadel/client'
|
|
11
15
|
|
|
12
16
|
module PlatformSdk
|
|
13
17
|
module Identity
|
data/lib/platform_sdk/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: strongmind-platform-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.33.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Platform Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: faraday
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -305,6 +319,9 @@ files:
|
|
|
305
319
|
- lib/platform_sdk/identity.rb
|
|
306
320
|
- lib/platform_sdk/identity/clients.rb
|
|
307
321
|
- lib/platform_sdk/identity/error_handleable.rb
|
|
322
|
+
- lib/platform_sdk/identity/zitadel/auth_client.rb
|
|
323
|
+
- lib/platform_sdk/identity/zitadel/client.rb
|
|
324
|
+
- lib/platform_sdk/identity/zitadel/user_payload.rb
|
|
308
325
|
- lib/platform_sdk/jira.rb
|
|
309
326
|
- lib/platform_sdk/jira/service_management_client.rb
|
|
310
327
|
- lib/platform_sdk/jobs/send_noun_to_pipeline_job.rb
|