kinetic_sdk 7.0.0.rc2 → 7.0.0.rc4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d17b093333b568589b62518a2d39f94d34bc0a82833a18df3aa0fae4c9e4d913
4
- data.tar.gz: e90ffa58599c343b80e7907102f3a7f858035f1150de4a8fd0a3f5b151dcf66a
3
+ metadata.gz: 0ab81a960e1aebbf2e7cd839fdf3fd60c33ca71e985e0c6530c7e84ca8e8ec6f
4
+ data.tar.gz: d485a88f2085aa142c0015c2899999d46c403f66b8b460efffb0918730ca51aa
5
5
  SHA512:
6
- metadata.gz: 78a37fe4b1776e46ebe64d64e4c779967cec791292474378155cca09a835a5041e12deea60ba28d7f713621ded57f2dfeb99b50cd14e9d5b802354a885a9bb70
7
- data.tar.gz: c3f15b84bbb614618944866ab1f3d5f59eea9875a1ef23a3e4452079eca425e9d5a0eae872345606e063d6be3b35654791b348ba4019a382646afd44afde1637
6
+ metadata.gz: dbd1dbf0607dc20f54e2cbea981496d1fbc8088c8b00fa8ce6483317577fe15bff2b8ae3328d7e797506dea4db66a9cc5c6d3b3ca491bdc99a0ff6fd144e9725
7
+ data.tar.gz: 86092ddc85c919ace2d1bb3133c8feb9aa320969162e471cc100617b81fef05fa0dc5b644c770dbb1a41ebc8c4b6e8aa53586bcc31463b966caef2fb44aa74fe
data/CHANGELOG.md CHANGED
@@ -1,6 +1,70 @@
1
1
  # Change Log
2
2
 
3
3
 
4
+ ## [7.0.0.rc4](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0.rc4) (2026-09-01)
5
+
6
+ **Breaking changes:**
7
+
8
+ - `jwt_token` now uses the OAuth 2.0 `client_credentials` grant, a single POST to
9
+ `/{space_slug}/app/oauth2/token`. Kinetic Platform 7 runs a standards compliant
10
+ authorization server in which `/app/oauth2/authorize` is an interactive endpoint
11
+ requiring a browser session, consent, a registered redirect_uri and PKCE, so the
12
+ previous authorization-code flow returned `400`. The response shape is unchanged:
13
+ callers reading `jwt_response.content["access_token"]` are unaffected.
14
+ - `jwt_code` has been removed. It drove the authorization-code flow that no longer
15
+ applies to machine to machine clients.
16
+ - The OAuth client used by the SDK must be registered in the space as a **confidential**
17
+ client (or with no `clientType`, which defaults to confidential). Public clients and
18
+ the built in system client register only the `authorization_code` grant and are
19
+ rejected by the token endpoint.
20
+
21
+ **Fixed bugs:**
22
+
23
+ - The OAuth routes are now space scoped (`/{space_slug}/app/oauth2/...`) when the SDK is
24
+ built from an `:app_server_url` plus a `:space_slug`. The authorization server resolves
25
+ the space from the request path and rejects requests made outside a space context.
26
+ - OAuth failures now report the `error` and `error_description` from a JSON error, or the
27
+ `X-Kinetic-CID` correlation id when Core answers with its generic HTML error page,
28
+ instead of dumping the inspected response object.
29
+
30
+ **Implemented enhancements:**
31
+
32
+ - Added the `:oauth_scope` option, defaulting to `full`. Valid scopes are `full`, `read`,
33
+ `write`, `admin`, `submissions` and `submissions:read`.
34
+ - Added specs covering the token exchange, scope configuration, and the failure modes for
35
+ both the Integrator and Discussions SDKs.
36
+
37
+
38
+ ## [7.0.0.rc3](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0.rc3) (2026-09-01)
39
+
40
+ **Fixed bugs:**
41
+
42
+ - Fixed a crash in `import_trees` and `import_trees_threaded` that aborted template installs
43
+ with `NoMethodError: undefined method 'document' for an instance of File`. The tree
44
+ comparison introduced in 7.x passed a raw `File` handle to `REXML::XPath.first`, which
45
+ requires a parsed document.
46
+ - Removed the tree comparison entirely, restoring the unconditional import behavior of 5.0.29.
47
+ The comparison identified trees by a `//definitionId` element that exists only in routine
48
+ exports, never in the tree exports `import_trees` reads, so it could not succeed. See the
49
+ specs in `spec/kinetic_sdk/task/trees_spec.rb`.
50
+ - The per-tree and per-routine rescues in the threaded importers no longer discard exceptions
51
+ silently; failures are logged and propagated.
52
+ - `trees.rb` now requires `rexml/document` explicitly rather than relying on load order.
53
+
54
+ **Implemented enhancements:**
55
+
56
+ - Added rspec and the first unit test coverage, run with `bundle exec rspec` or `rake spec`.
57
+
58
+
59
+ ## [7.0.0.rc2](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0.rc2) (2026-09-01)
60
+
61
+ **Fixed bugs:**
62
+
63
+ - Declared `concurrent-ruby` and `rexml` as runtime dependencies. Both were required by the
64
+ library but missing from the gemspec, so the published gem could fail to load.
65
+ - Pinned `BUNDLED WITH` to a concrete Bundler version; the range value broke `bundle install`.
66
+
67
+
4
68
  ## [7.0.0-rc1](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0-rc1) (2026-08-31)
5
69
 
6
70
  **Breaking changes:**
data/Rakefile CHANGED
@@ -7,7 +7,19 @@ YARD::Rake::YardocTask.new do |t|
7
7
  t.stats_options = %w( --list-undoc )
8
8
  end
9
9
 
10
+ # Run the specs. Guarded so the Rakefile still loads (and `rake build` still
11
+ # works) in an environment where the development dependencies are absent.
12
+ begin
13
+ require "rspec/core/rake_task"
14
+ RSpec::Core::RakeTask.new(:spec)
15
+ rescue LoadError
16
+ desc "Run the specs (rspec is not installed)"
17
+ task :spec do
18
+ abort "rspec is not available. Run `bundle install` first."
19
+ end
20
+ end
21
+
10
22
  # Generate Yard documentation
11
23
  task :doc => [:yard]
12
24
  task :rdoc => [:yard]
13
- task :default => [:doc]
25
+ task :default => [:spec, :doc]
data/kinetic_sdk.gemspec CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "bundler", ">= 1.17"
27
27
  spec.add_development_dependency "rake", ">= 13.0.1"
28
28
  spec.add_development_dependency "yard", "~> 0.9.45"
29
+ spec.add_development_dependency "rspec", "~> 3.13"
29
30
 
30
31
  spec.metadata["yard.run"] = "yri"
31
32
  end
@@ -14,7 +14,8 @@ module KineticSdk
14
14
  include KineticSdk::Utils::KineticExportUtils
15
15
 
16
16
  attr_reader :api_url, :username, :options, :password, :proxy_url,
17
- :space_slug, :server, :version, :logger
17
+ :space_slug, :server, :version, :logger, :oauth_url,
18
+ :oauth_scope
18
19
 
19
20
  # Initalize the Core SDK with the web server URL, the space user
20
21
  # username and password, along with any custom option values.
@@ -55,6 +56,8 @@ module KineticSdk
55
56
  # * :max_redirects (Fixnum) (_defaults to: 5_) maximum number of redirects to follow
56
57
  # * :ssl_ca_file (String) full path to PEM certificate used to verify the server
57
58
  # * :ssl_verify_mode (String) (_defaults to: none_) - none | peer
59
+ # * :oauth_scope (String) (_defaults to: full_) scope requested when retrieving a
60
+ # JWT - full | read | write | admin | submissions | submissions:read
58
61
  #
59
62
  # Example: using a configuration file
60
63
  #
@@ -154,12 +157,17 @@ module KineticSdk
154
157
  @server = options[:app_server_url].chomp('/')
155
158
  @api_url = @server + (@space_slug.nil? ? "/app/api/v1" : "/#{@space_slug}/app/api/v1")
156
159
  @proxy_url = @space_slug.nil? ? nil : "#{@server}/#{@space_slug}/app/components"
160
+ # The authorization server resolves the space from the request path, so the
161
+ # OAuth routes must be space scoped exactly like the API routes.
162
+ @oauth_url = @server + (@space_slug.nil? ? "/app/oauth2" : "/#{@space_slug}/app/oauth2")
157
163
  else
158
164
  raise StandardError.new "The :space_slug option is required when using the :space_server_url option" if @space_slug.nil?
159
165
  @server = options[:space_server_url].chomp('/')
160
166
  @api_url = "#{@server}/app/api/v1"
161
167
  @proxy_url = "#{@server}/app/components"
168
+ @oauth_url = "#{@server}/app/oauth2"
162
169
  end
170
+ @oauth_scope = @options[:oauth_scope] || @options["oauth_scope"] || "full"
163
171
  @version = 1
164
172
  end
165
173
 
@@ -1,55 +1,79 @@
1
1
  module KineticSdk
2
2
  class Core
3
3
 
4
- # Gets an authentication token
4
+ # Gets an authentication token using the OAuth 2.0 client credentials grant.
5
+ #
6
+ # Kinetic Platform 7 runs a standards compliant authorization server, in which
7
+ # `/app/oauth2/authorize` is an interactive endpoint: it expects an authenticated
8
+ # browser session, a consent round trip, a registered redirect_uri, and PKCE. A
9
+ # machine to machine client cannot drive it, and should not try - it exchanges its
10
+ # own credentials for a token directly at the token endpoint instead.
11
+ #
12
+ # The OAuth client must be registered in the space as a confidential client (or with
13
+ # no clientType, which defaults to confidential) so that it carries the
14
+ # `client_credentials` grant. Public clients and the built in system client support
15
+ # only `authorization_code`, and will be rejected here.
5
16
  #
6
17
  # @param client_id [String] the oauth client id
7
18
  # @param client_secret [String] the oauth client secret
8
- # @param headers [Hash] hash of headers to send, default is basic authentication and accept JSON content type
19
+ # @param headers [Hash] additional headers to send. The Accept, Authorization and
20
+ # Content-Type headers required by the token endpoint always take precedence.
21
+ # @param scope [String] scope to request, defaults to the +:oauth_scope+ option (+full+)
9
22
  # @return [KineticSdk::Utils::KineticHttpResponse] object, with +code+, +message+, +content_string+, and +content+ properties
10
- def jwt_token(client_id, client_secret, headers = default_headers)
11
- # retrieve the jwt code
12
- jwt_code = jwt_code(client_id, headers)
13
- # retrieve the jwt token
23
+ def jwt_token(client_id, client_secret, headers = {}, scope = oauth_scope)
14
24
  @logger.info("Retrieving JWT authorization token")
15
- url = "#{@server}/app/oauth2/token?grant_type=authorization_code&response_type=token&client_id=#{client_id}&code=#{jwt_code}"
16
- token_headers = header_accept_json.merge(header_basic_auth(client_id, client_secret))
17
- response = post(url, {}, token_headers, { :max_redirects => 0 })
25
+ url = "#{@oauth_url}/token"
26
+
27
+ # The required headers are merged last so that a caller passing the SDK default
28
+ # headers cannot replace the client's basic authentication with the user's.
29
+ token_headers = headers
30
+ .merge(header_accept_json)
31
+ .merge(header_basic_auth(client_id, client_secret))
32
+ .merge({ "Content-Type" => "application/x-www-form-urlencoded" })
33
+
34
+ payload = { "grant_type" => "client_credentials" }
35
+ payload["scope"] = scope unless scope.nil? || scope.to_s.empty?
18
36
 
19
- if response.status == 401
20
- raise StandardError.new "#{response.message}, the oauth client id and secret are invalid."
21
- elsif response.status == 200
37
+ # Redirects are not followed: the token endpoint has no reason to redirect, and
38
+ # following one would forward the client credentials to another host.
39
+ response = post(url, URI.encode_www_form(payload), token_headers, { :max_redirects => 0 })
40
+
41
+ case response.status
42
+ when 200
22
43
  response
44
+ when 401
45
+ raise StandardError.new(
46
+ "Unable to retrieve token: #{oauth_error_message(response)}. The oauth client id " \
47
+ "and secret are invalid, or no such client is registered in this space."
48
+ )
23
49
  else
24
- raise StandardError.new "Unable to retrieve token: #{response}"
50
+ raise StandardError.new("Unable to retrieve token: #{oauth_error_message(response)}")
25
51
  end
26
52
  end
27
53
 
28
- # Gets an authentication code.
54
+ private
55
+
56
+ # Builds an actionable message from an OAuth error response.
29
57
  #
30
- # This method should really never need to be called externally.
58
+ # The authorization server answers most failures with a JSON OAuth error, but falls
59
+ # back to Core's generic HTML error page for others. That page carries no error code,
60
+ # only a correlation id in the X-Kinetic-CID response header, which is what is needed
61
+ # to find the matching entry in the server log.
31
62
  #
32
- # @param client_id [String]
33
- # @param headers [Hash] hash of headers to send, default is basic authentication and accept JSON content type
34
- # @return [KineticSdk::Utils::KineticHttpResponse] object, with +code+, +message+, +content_string+, and +content+ properties
35
- def jwt_code(client_id, headers = default_headers)
36
- @logger.info("Retrieving JWT authorization code")
37
- url = "#{@server}/app/oauth2/authorize?grant_type=authorization_code&response_type=code&client_id=#{client_id}"
38
- response = post(url, {}, headers, { :max_redirects => -1 })
39
-
40
- if response.status == 401
41
- raise StandardError.new "#{response.message}: #{response.content["error"]}"
42
- elsif response.status == 302 || response.status == 303
43
- location = response.headers["location"]
44
- if location.nil?
45
- raise StandardError.new "Unable to retrieve code: #{response.inspect}"
46
- elsif !location.include?("?code=")
47
- raise StandardError.new "Unable to retrieve code, the authorize endpoint redirected to #{location} without an authorization code."
48
- else
49
- location.split("?code=").last.split("#/").first
50
- end
63
+ # @param response [KineticSdk::Utils::KineticHttpResponse] the failed response
64
+ # @return [String] a single line description of the failure
65
+ def oauth_error_message(response)
66
+ content = response.content.is_a?(Hash) ? response.content : {}
67
+ if content["error"]
68
+ message = "#{response.status} #{content["error"]}"
69
+ message += " - #{content["error_description"]}" if content["error_description"]
70
+ message
51
71
  else
52
- raise StandardError.new "Unable to retrieve code #{response.inspect}"
72
+ message = "#{response.status} #{response.message}"
73
+ headers = response.headers
74
+ correlation_id = headers.is_a?(Hash) ? headers["x-kinetic-cid"] : nil
75
+ message += " (correlation id #{correlation_id})" if correlation_id
76
+ message
53
77
  end
54
78
  end
55
79
  end
@@ -1,3 +1,5 @@
1
+ require 'rexml/document'
2
+
1
3
  module KineticSdk
2
4
  class Task
3
5
  # Delete a tree.
@@ -146,12 +148,7 @@ module KineticSdk
146
148
  @logger.info("Importing all Trees from Export Directory")
147
149
  Dir["#{@options[:export_directory]}/sources/**/*.xml"].sort.each do |file|
148
150
  tree_file = File.new(file, "rb")
149
- match = compare_trees(tree_file)
150
- if !match
151
- import_tree(tree_file, force_overwrite, headers)
152
- else
153
- #Matching trees, skipping
154
- end
151
+ import_tree(tree_file, force_overwrite, headers)
155
152
  end
156
153
  end
157
154
  def import_trees_threaded(force_overwrite=false, headers=header_basic_auth)
@@ -164,48 +161,18 @@ module KineticSdk
164
161
  Dir["#{@options[:export_directory]}/sources/**/*.xml"].sort.each do |file|
165
162
  promises << Concurrent::Promise.execute(executor: pool) do
166
163
  begin
167
-
168
164
  tree_file = File.new(file, "rb")
169
- match = compare_trees(tree_file)
170
- if !match
171
- import_tree(tree_file, force_overwrite, headers)
172
- else
173
- #Matching trees, skipping
174
- end
175
- rescue
165
+ import_tree(tree_file, force_overwrite, headers)
166
+ rescue => e
167
+ @logger.error("Failed to import Tree #{file}: #{e.class}: #{e.message}")
168
+ raise
176
169
  end
177
-
178
170
  end
179
171
  end
180
172
  promises.each(&:wait!)
181
173
  pool.shutdown
182
174
  pool.wait_for_termination
183
175
  end
184
- # Import a tree
185
- #
186
- # If the tree already exists on the server, this will fail unless forced
187
- # to overwrite.
188
- #
189
- # The source named in the tree content must also exist on the server, or
190
- # the import will fail.
191
- #
192
- # @param tree [String] content from tree file
193
- # @param force_overwrite [Boolean] whether to overwrite a tree if it exists, default is false
194
- # @param headers [Hash] hash of headers to send, default is basic authentication
195
- # @return [KineticSdk::Utils::KineticHttpResponse] object, with +code+, +message+, +content_string+, and +content+ properties
196
- def compare_trees(tree, headers=header_basic_auth)
197
- definitionId = REXML::XPath.first(tree, "//definitionId")&.text
198
- @logger.info("Querying for Tree #{definitionId}")
199
- response = find_tree(definitionId, { "include" => "details,export" })
200
- @logger.info("Comparing new and old tree")
201
- prev_tree = response.content
202
- if prev_tree == tree
203
- return true
204
- else
205
- return false
206
- end
207
- end
208
-
209
176
  # Import a routine
210
177
  #
211
178
  # If the routine already exists on the server, this will fail unless
@@ -248,9 +215,11 @@ module KineticSdk
248
215
  Dir["#{@options[:export_directory]}/routines/*.xml"].sort.each do |file|
249
216
  promises << Concurrent::Promise.execute(executor: pool) do
250
217
  begin
251
- routine_file = File.new(file, "rb")
252
- import_routine(routine_file, force_overwrite, headers)
253
- rescue
218
+ routine_file = File.new(file, "rb")
219
+ import_routine(routine_file, force_overwrite, headers)
220
+ rescue => e
221
+ @logger.error("Failed to import Routine #{file}: #{e.class}: #{e.message}")
222
+ raise
254
223
  end
255
224
  end
256
225
  end
@@ -3,5 +3,5 @@ module KineticSdk
3
3
  # Version of Kinetic SDK
4
4
  #
5
5
  # @return [String] Version of the SDK
6
- VERSION = "7.0.0.rc2"
6
+ VERSION = "7.0.0.rc4"
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kinetic_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0.rc2
4
+ version: 7.0.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinetic Data
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: 0.9.45
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.13'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.13'
125
139
  description:
126
140
  email:
127
141
  - support@kineticdata.com