k9o 1.2.0-aarch64-linux → 1.5.0-aarch64-linux
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 +115 -1
- data/README.md +1575 -180
- data/k9o.gemspec +2 -2
- data/lib/k9o/_ffi/kibo_kms_rest_ffi.rb +928 -25
- data/lib/k9o/_ffi/libkibo_kms_rest_ffi.so +0 -0
- data/lib/k9o/ai.rb +15 -0
- data/lib/k9o/analytics.rb +403 -0
- data/lib/k9o/approvals.rb +253 -0
- data/lib/k9o/audit.rb +162 -14
- data/lib/k9o/client.rb +28 -9
- data/lib/k9o/compute.rb +32 -15
- data/lib/k9o/crypto.rb +471 -0
- data/lib/k9o/custody.rb +28 -0
- data/lib/k9o/data.rb +125 -5
- data/lib/k9o/ed25519.rb +182 -0
- data/lib/k9o/end_users.rb +93 -0
- data/lib/k9o/envelope.rb +20 -5
- data/lib/k9o/functions.rb +127 -9
- data/lib/k9o/gate.rb +52 -22
- data/lib/k9o/http_transport.rb +78 -16
- data/lib/k9o/jcs.rb +117 -0
- data/lib/k9o/kerror.rb +57 -5
- data/lib/k9o/keys.rb +131 -0
- data/lib/k9o/kibo.rb +3 -3
- data/lib/k9o/kibo_client_core.rb +385 -0
- data/lib/k9o/kibo_ffi_core.rb +178 -0
- data/lib/k9o/locate.rb +118 -0
- data/lib/k9o/me.rb +27 -87
- data/lib/k9o/metadata.rb +342 -0
- data/lib/k9o/moat.rb +443 -0
- data/lib/k9o/org.rb +81 -68
- data/lib/k9o/orgs.rb +1 -0
- data/lib/k9o/policies.rb +25 -6
- data/lib/k9o/projects.rb +43 -8
- data/lib/k9o/query.rb +174 -0
- data/lib/k9o/rampart.rb +273 -0
- data/lib/k9o/realtime.rb +261 -32
- data/lib/k9o/region.rb +20 -18
- data/lib/k9o/schedules.rb +109 -0
- data/lib/k9o/schema.rb +35 -0
- data/lib/k9o/streams.rb +94 -0
- data/lib/k9o/studio.rb +21 -11
- data/lib/k9o/url_grammar.rb +46 -20
- data/lib/k9o/vault.rb +79 -84
- data/lib/k9o/vectors.rb +124 -0
- data/lib/k9o/version.rb +1 -1
- data/lib/k9o.rb +17 -0
- data/sig/k9o/ai.rbs +4 -0
- data/sig/k9o/approvals.rbs +71 -0
- data/sig/k9o/datasets.rbs +125 -0
- data/sig/k9o/gate.rbs +9 -6
- data/sig/k9o/kerror.rbs +24 -0
- data/sig/k9o/kibo_client_core.rbs +25 -0
- data/sig/k9o/metadata.rbs +54 -0
- data/sig/k9o/moat.rbs +29 -0
- data/sig/k9o/query.rbs +26 -0
- data/sig/k9o/rampart.rbs +59 -0
- data/sig/k9o/realtime.rbs +34 -0
- data/sig/k9o/region.rbs +14 -3
- data/sig/k9o/workload.rbs +64 -0
- metadata +33 -4
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'kerror'
|
|
4
|
+
|
|
5
|
+
module K9o
|
|
6
|
+
# The bound Kibo core for K9o::KiboClientCore: it hands the SDK's plain Hashes and Strings to the generated uniffi
|
|
7
|
+
# binding (+KiboKmsRestFfi+, zk-bundle-v2.16.3 or later) as the typed records, integer enums and federation variant
|
|
8
|
+
# the binding wants, and turns the binding's errors into the SDK's "CODE: detail" form. There is no cryptography here:
|
|
9
|
+
# sealing, opening, the key pin and the token exchange all run in the native core. Requiring +k9o+ does not load the
|
|
10
|
+
# binding; building this object does, and raises a K9o::KError saying what is missing when it cannot.
|
|
11
|
+
#
|
|
12
|
+
# core = K9o::KiboFfiCore.new # binding and library on the default paths
|
|
13
|
+
# core = K9o::KiboFfiCore.new(library: '/opt/kibo/libkibo_kms_rest_ffi.so') # library at an absolute path
|
|
14
|
+
#
|
|
15
|
+
# The generated binding opens the native library by its bare name, +kibo_kms_rest_ffi+. Ruby's +ffi+ gem looks for it in
|
|
16
|
+
# the system library paths (+/opt/homebrew/lib+, +/opt/local/lib+, +/usr/local/lib+, +/usr/lib+, and on Linux the
|
|
17
|
+
# loader paths such as +LD_LIBRARY_PATH+). macOS does not pass +DYLD_LIBRARY_PATH+ to the system Ruby, so on macOS put
|
|
18
|
+
# the library in one of those directories or pass +library:+ with its absolute path. +library:+ makes this adapter load
|
|
19
|
+
# the binding with that path in place of the bare name; the binding file itself is not changed.
|
|
20
|
+
class KiboFfiCore
|
|
21
|
+
# Raised from the binding's errors. The message is "<CODE>: <detail>", which KiboClientCore turns into the item's
|
|
22
|
+
# code and message.
|
|
23
|
+
class CoreError < StandardError; end
|
|
24
|
+
|
|
25
|
+
PROVIDERS = { 'aws-kms' => :AWS_KMS, 'gcp-kms' => :GCP_KMS, 'azure-kv' => :AZURE_KV,
|
|
26
|
+
'hashicorp-vault' => :HASHICORP_VAULT }.freeze
|
|
27
|
+
POSTURES = { 'byok' => :BYOK, 'hyok' => :HYOK }.freeze
|
|
28
|
+
BINDING_LIB_LINE = /^(\s*)ffi_lib\s+'kibo_kms_rest_ffi'\s*$/.freeze
|
|
29
|
+
REQUIRED = %i[app_crypto_seal_blocking app_crypto_open_blocking acquire_kms_credential_blocking
|
|
30
|
+
core_capabilities].freeze
|
|
31
|
+
|
|
32
|
+
# +library+: absolute path of the native library, for a library that is not on the default paths.
|
|
33
|
+
# +binding_path+: path of the generated +kibo_kms_rest_ffi.rb+, when it is not on the load path. Needed only if
|
|
34
|
+
# +KiboKmsRestFfi+ is not already loaded and +require 'kibo_kms_rest_ffi'+ would not find it.
|
|
35
|
+
# +ffi+: the binding module itself, when you have already loaded it under another name.
|
|
36
|
+
def initialize(library: nil, binding_path: nil, ffi: nil)
|
|
37
|
+
@ffi = ffi || load_binding(library, binding_path)
|
|
38
|
+
REQUIRED.each do |fn|
|
|
39
|
+
next if @ffi.respond_to?(fn)
|
|
40
|
+
|
|
41
|
+
raise unavailable("the loaded KiboKmsRestFfi does not export #{fn}; it is older than zk-bundle-v2.16.3")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def core_capabilities
|
|
46
|
+
guard { @ffi.core_capabilities }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def acquire_kms_credential_blocking(input)
|
|
50
|
+
guard do
|
|
51
|
+
@ffi.acquire_kms_credential_blocking(
|
|
52
|
+
@ffi::FfiFederationInput.new(spec: spec(input[:spec]), identity_token: input[:identity_token],
|
|
53
|
+
now_unix_secs: input[:now_unix_secs])
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def app_crypto_seal_blocking(input)
|
|
59
|
+
guard do
|
|
60
|
+
@ffi.app_crypto_seal_blocking(
|
|
61
|
+
@ffi::AppSealInput.new(
|
|
62
|
+
provider: enum(@ffi::KmsProvider, PROVIDERS, input[:provider], 'provider'),
|
|
63
|
+
posture: enum(@ffi::FfiPosture, POSTURES, input[:posture], 'posture'),
|
|
64
|
+
kms_cred: cred(input[:kms_cred]),
|
|
65
|
+
key_ref: @ffi::FfiKeyRef.new(version: input[:key_ref][:version], external_ref: input[:key_ref][:external_ref]),
|
|
66
|
+
provider_config: provider_config(input[:provider_config]),
|
|
67
|
+
project_id: input[:project_id], crypt: input[:crypt], context: input[:context],
|
|
68
|
+
plaintext: input[:plaintext], expected_key_refs: input[:expected_key_refs]
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def app_crypto_open_blocking(input)
|
|
75
|
+
guard do
|
|
76
|
+
@ffi.app_crypto_open_blocking(
|
|
77
|
+
@ffi::AppOpenInput.new(
|
|
78
|
+
kms_cred: cred(input[:kms_cred]), provider_config: provider_config(input[:provider_config]),
|
|
79
|
+
project_id: input[:project_id], crypt: input[:crypt], context: input[:context],
|
|
80
|
+
container: input[:container], expected_key_refs: input[:expected_key_refs]
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def inspect
|
|
87
|
+
"#<#{self.class.name}>"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def unavailable(message)
|
|
93
|
+
KError.new(code: 'KIBO_WASM_ERROR', message: message,
|
|
94
|
+
recommendation: 'Install the generated Kibo binding (kibo_kms_rest_ffi.rb) and the native library ' \
|
|
95
|
+
'(zk-bundle-v2.16.3 or later); see "Encrypt through client.crypto" in the README.')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def load_binding(library, binding_path)
|
|
99
|
+
require 'ffi'
|
|
100
|
+
existing = loaded_binding
|
|
101
|
+
return existing if existing
|
|
102
|
+
return require_binding(binding_path) if library.nil?
|
|
103
|
+
|
|
104
|
+
load_with_library(library, binding_path)
|
|
105
|
+
rescue LoadError => e
|
|
106
|
+
raise unavailable("the Kibo binding or its native library could not be loaded: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def loaded_binding
|
|
110
|
+
return nil unless Object.const_defined?(:KiboKmsRestFfi)
|
|
111
|
+
|
|
112
|
+
mod = Object.const_get(:KiboKmsRestFfi)
|
|
113
|
+
mod.respond_to?(:app_crypto_seal_blocking) ? mod : nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def require_binding(binding_path)
|
|
117
|
+
binding_path ? require(File.expand_path(binding_path)) : require('kibo_kms_rest_ffi')
|
|
118
|
+
Object.const_get(:KiboKmsRestFfi)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The binding opens +ffi_lib 'kibo_kms_rest_ffi'+ at load. To use an absolute path, the file is read and loaded with
|
|
122
|
+
# that one line pointing at the path; nothing on disk changes.
|
|
123
|
+
def load_with_library(library, binding_path)
|
|
124
|
+
path = File.expand_path(library)
|
|
125
|
+
raise LoadError, "no native library at #{path}" unless File.file?(path)
|
|
126
|
+
|
|
127
|
+
file = binding_path ? File.expand_path(binding_path) : $LOAD_PATH.map { |d| File.join(d.to_s, 'kibo_kms_rest_ffi.rb') }.find { |f| File.file?(f) }
|
|
128
|
+
raise LoadError, 'cannot find kibo_kms_rest_ffi.rb; pass binding_path: with its path' unless file
|
|
129
|
+
|
|
130
|
+
source = File.read(file)
|
|
131
|
+
raise LoadError, "#{file} does not look like the generated Kibo binding" unless source.match?(BINDING_LIB_LINE)
|
|
132
|
+
|
|
133
|
+
# rubocop:disable Security/Eval
|
|
134
|
+
TOPLEVEL_BINDING.eval(source.sub(BINDING_LIB_LINE) { "#{Regexp.last_match(1)}ffi_lib #{path.inspect}" }, file)
|
|
135
|
+
# rubocop:enable Security/Eval
|
|
136
|
+
Object.const_get(:KiboKmsRestFfi)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def enum(mod, table, wire, field)
|
|
140
|
+
name = table[wire.to_s]
|
|
141
|
+
raise KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: "unknown #{field} #{wire.inspect}", context: { 'field' => field }) unless name
|
|
142
|
+
|
|
143
|
+
mod.const_get(name)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def cred(h)
|
|
147
|
+
@ffi::FfiCredential.new(secret: h[:secret], expires_at_unix_secs: h[:expires_at_unix_secs])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def provider_config(h)
|
|
151
|
+
h ||= {}
|
|
152
|
+
@ffi::FfiProviderConfig.new(aws_region: h[:aws_region], vault_address: h[:vault_address])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The core's federation spec is a variant class per cloud.
|
|
156
|
+
def spec(s)
|
|
157
|
+
v = @ffi::FfiFederationSpec
|
|
158
|
+
case s[:provider].to_s
|
|
159
|
+
when 'aws' then v::AWS.new(s[:role_arn], s[:session_name], s[:region], s[:key_arn], s[:duration_secs])
|
|
160
|
+
when 'gcp' then v::GCP.new(s[:audience])
|
|
161
|
+
when 'azure' then v::AZURE.new(s[:tenant_id], s[:client_id], s[:scope])
|
|
162
|
+
when 'vault' then v::VAULT.new(s[:vault_addr], s[:mount], s[:role])
|
|
163
|
+
else
|
|
164
|
+
raise KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: "unknown federation provider #{s[:provider].inspect}",
|
|
165
|
+
context: { 'field' => 'federation.provider' })
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The binding's error classes keep the code and detail as readers; their +message+ is not "CODE: detail".
|
|
170
|
+
def guard
|
|
171
|
+
yield
|
|
172
|
+
rescue @ffi::ZkFfiUniffiError::Platform, @ffi::ZkFfiUniffiError::PostureViolation => e
|
|
173
|
+
raise CoreError, "#{e.code}: #{e.detail}"
|
|
174
|
+
rescue @ffi::ZkFfiUniffiError::UpstreamKms => e
|
|
175
|
+
raise CoreError, "UPSTREAM_KMS_ERROR: #{e.detail}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/k9o/locate.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module K9o
|
|
6
|
+
# Locating the end user's region for password sign-in.
|
|
7
|
+
#
|
|
8
|
+
# K9o::Region finds the org's regional API host. An end user can live in another region of the project, so
|
|
9
|
+
# +gate.sign_in+ first asks the project which region holds the address
|
|
10
|
+
# (+POST /@<project>/gate/locate { email }+ at that regional base, with no credential) and sends the password to the
|
|
11
|
+
# origin it names. The answer is the same for a known and an unknown address.
|
|
12
|
+
#
|
|
13
|
+
# Rules:
|
|
14
|
+
# - The located origin is used for that one attempt. It is never cached. The one thing kept is a hint per refresh
|
|
15
|
+
# token (at most 64, in memory), so that refresh and sign-out reach the region that issued the session; a stale
|
|
16
|
+
# hint gets a 401.
|
|
17
|
+
# - An origin is used only if it is https (http only when the base is itself http on a loopback host and the origin is
|
|
18
|
+
# loopback too), has no userinfo, port, path, query or fragment, and its host is exactly the base host's org,
|
|
19
|
+
# family and apex with the region code ([a-z]{2}\d{2}) added or swapped in the gateway label, and that code equals
|
|
20
|
+
# the answer's +region+. Anything else, or +origin: null+, means the base is used. A password never goes to a host
|
|
21
|
+
# that fails this.
|
|
22
|
+
module Locate
|
|
23
|
+
REGION_CODE = /\A[a-z]{2}\d{2}\z/.freeze
|
|
24
|
+
HINT_LIMIT = 64
|
|
25
|
+
HINT_LOCK = Mutex.new
|
|
26
|
+
HINTS = {}
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def loopback?(host)
|
|
31
|
+
h = host.to_s.downcase
|
|
32
|
+
h == 'localhost' || h.end_with?('.localhost') || h == '127.0.0.1' || h == '[::1]' || h == '::1'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The origin the credential goes to: the located one if it passes the rule, else +base+. Pure; never raises.
|
|
36
|
+
# +answer+ is the +data+ of the locate envelope.
|
|
37
|
+
def choose_origin(base, answer)
|
|
38
|
+
return base unless answer.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
region = answer['region'] || answer[:region]
|
|
41
|
+
origin = answer['origin'] || answer[:origin]
|
|
42
|
+
return base unless origin.is_a?(String) && region.is_a?(String) && REGION_CODE.match?(region)
|
|
43
|
+
|
|
44
|
+
b = URI.parse(base)
|
|
45
|
+
u = URI.parse(origin)
|
|
46
|
+
return base if b.host.nil? || u.host.nil? || !u.scheme.is_a?(String)
|
|
47
|
+
return base unless u.userinfo.nil? && u.query.nil? && u.fragment.nil?
|
|
48
|
+
return base unless u.port == u.default_port
|
|
49
|
+
return base unless u.path.empty? || u.path == '/'
|
|
50
|
+
|
|
51
|
+
bs = b.scheme.to_s.downcase
|
|
52
|
+
us = u.scheme.to_s.downcase
|
|
53
|
+
unless us == 'https' || (us == 'http' && bs == 'http' && loopback?(b.host) && loopback?(u.host))
|
|
54
|
+
return base
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
bh = b.host.downcase
|
|
58
|
+
uh = u.host.downcase
|
|
59
|
+
return "#{bs}://#{bh}#{b.port == b.default_port ? '' : ":#{b.port}"}" if us == bs && uh == bh && u.port == b.port
|
|
60
|
+
|
|
61
|
+
from = UrlGrammar.parse_api_host(bh)
|
|
62
|
+
to = UrlGrammar.parse_api_host(uh)
|
|
63
|
+
return base if from.nil? || to.nil? || to.region != region
|
|
64
|
+
return base unless from.org_slug == to.org_slug && from.family == to.family && from.apex == to.apex
|
|
65
|
+
|
|
66
|
+
"#{us}://#{uh}"
|
|
67
|
+
rescue URI::Error, ArgumentError
|
|
68
|
+
base
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Ask the project which region holds +email+ and return the origin the credential goes to. A 404 from the door
|
|
72
|
+
# means the locate door is off and the base is used. Any other non-2xx answer (429, 503, anything else) raises as any other gateway error does, and nothing is sent after it. A 2xx
|
|
73
|
+
# answer that cannot be read means the base is used.
|
|
74
|
+
def origin_for(config, email, path)
|
|
75
|
+
base = Region.resolve_origin(config).sub(%r{/+\z}, '')
|
|
76
|
+
begin
|
|
77
|
+
env = HttpTransport.request(config: config, method: 'POST', path: path, body: { 'email' => email },
|
|
78
|
+
authenticated: false)
|
|
79
|
+
rescue KError => e
|
|
80
|
+
# 404: the locate door is off, so the base is used.
|
|
81
|
+
raise unless e.status == 404 || (e.code == 'K9O_INTERNAL_ERROR' && e.status.to_i.between?(200, 299))
|
|
82
|
+
|
|
83
|
+
return base
|
|
84
|
+
end
|
|
85
|
+
choose_origin(base, env.data)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Remember which origin issued the session with this refresh token (a hint, not authority).
|
|
89
|
+
def remember(refresh_token, origin)
|
|
90
|
+
return unless refresh_token.is_a?(String) && !refresh_token.empty?
|
|
91
|
+
|
|
92
|
+
HINT_LOCK.synchronize do
|
|
93
|
+
HINTS.delete(refresh_token)
|
|
94
|
+
HINTS[refresh_token] = origin
|
|
95
|
+
HINTS.delete(HINTS.keys.first) while HINTS.size > HINT_LIMIT
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def hint(refresh_token)
|
|
100
|
+
HINT_LOCK.synchronize { HINTS[refresh_token] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def forget(refresh_token)
|
|
104
|
+
HINT_LOCK.synchronize { HINTS.delete(refresh_token) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A refresh answers with a new refresh token and the old one stops working: the hint follows the session. Data
|
|
108
|
+
# without a new token changes nothing.
|
|
109
|
+
def rotate(old_refresh_token, data)
|
|
110
|
+
token = data.is_a?(Hash) ? data['refreshToken'] : nil
|
|
111
|
+
origin = hint(old_refresh_token)
|
|
112
|
+
return unless token.is_a?(String) && !token.empty? && origin
|
|
113
|
+
|
|
114
|
+
forget(old_refresh_token)
|
|
115
|
+
remember(token, origin)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/k9o/me.rb
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module K9o
|
|
4
|
-
# Operator-self namespace — k9o.me.*. Cross-org;
|
|
5
|
-
#
|
|
6
|
-
# api_tokens / notifications / audit_log / data_export.
|
|
4
|
+
# Operator-self namespace — k9o.me.*. Cross-org; org-plane routes under /me.
|
|
5
|
+
# Sub-namespaces: profile / sessions / api_tokens.
|
|
7
6
|
class Me
|
|
8
|
-
attr_reader :profile, :sessions, :api_tokens
|
|
7
|
+
attr_reader :profile, :sessions, :api_tokens
|
|
9
8
|
|
|
10
9
|
def initialize(config)
|
|
11
10
|
@profile = Profile.new(config)
|
|
12
11
|
@sessions = Sessions.new(config)
|
|
13
12
|
@api_tokens = ApiTokens.new(config)
|
|
14
|
-
@notifications = Notifications.new(config)
|
|
15
|
-
@audit_log = AuditLog.new(config)
|
|
16
|
-
@data_export = DataExport.new(config)
|
|
17
13
|
end
|
|
18
14
|
|
|
19
15
|
# k9o.me.profile.*
|
|
20
16
|
class Profile
|
|
17
|
+
PATCH_FIELDS = { display_name: 'displayName', locale: 'locale', timezone: 'timezone' }.freeze
|
|
18
|
+
|
|
21
19
|
def initialize(config)
|
|
22
20
|
@config = config
|
|
23
21
|
end
|
|
@@ -26,9 +24,17 @@ module K9o
|
|
|
26
24
|
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me'))
|
|
27
25
|
end
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
# PATCH /me. Pass only the fields to change (display_name, locale, timezone); an explicit +nil+ is sent as
|
|
28
|
+
# null.
|
|
29
|
+
def update(**fields)
|
|
30
|
+
patch = {}
|
|
31
|
+
fields.each do |key, value|
|
|
32
|
+
wire = PATCH_FIELDS[key]
|
|
33
|
+
raise ArgumentError, "unknown profile field: #{key}" if wire.nil?
|
|
34
|
+
|
|
35
|
+
patch[wire] = value
|
|
36
|
+
end
|
|
37
|
+
HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('me'), body: patch)
|
|
32
38
|
end
|
|
33
39
|
end
|
|
34
40
|
|
|
@@ -42,13 +48,15 @@ module K9o
|
|
|
42
48
|
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/sessions'))
|
|
43
49
|
end
|
|
44
50
|
|
|
51
|
+
# POST /auth/sessions/<id>/revoke
|
|
45
52
|
def revoke(session_id)
|
|
46
|
-
HttpTransport.request(config: @config, method: '
|
|
47
|
-
path: UrlGrammar.org_path("
|
|
53
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
54
|
+
path: UrlGrammar.org_path("auth/sessions/#{enc(session_id)}/revoke"))
|
|
48
55
|
end
|
|
49
56
|
|
|
57
|
+
# POST /me/sessions/revoke-all
|
|
50
58
|
def revoke_all
|
|
51
|
-
HttpTransport.request(config: @config, method: '
|
|
59
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('me/sessions/revoke-all'))
|
|
52
60
|
end
|
|
53
61
|
|
|
54
62
|
private
|
|
@@ -68,10 +76,12 @@ module K9o
|
|
|
68
76
|
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/api-tokens'))
|
|
69
77
|
end
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
# POST /me/api-tokens { label, scopeManifest, ttlSeconds? }. +scope_manifest+ is a Hash with the gateway's keys
|
|
80
|
+
# (each may be nil for "no restriction"): 'tier' (readonly / readwrite / admin, required), 'projects', 'resourceClasses',
|
|
81
|
+
# 'collections', 'cidrs'. The response carries the token's plaintext once.
|
|
82
|
+
def create(label:, scope_manifest:, ttl_seconds: nil)
|
|
83
|
+
body = { 'label' => label, 'scopeManifest' => scope_manifest }
|
|
84
|
+
body['ttlSeconds'] = ttl_seconds unless ttl_seconds.nil?
|
|
75
85
|
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('me/api-tokens'), body: body)
|
|
76
86
|
end
|
|
77
87
|
|
|
@@ -86,75 +96,5 @@ module K9o
|
|
|
86
96
|
::URI.encode_www_form_component(s)
|
|
87
97
|
end
|
|
88
98
|
end
|
|
89
|
-
|
|
90
|
-
# k9o.me.notifications.*
|
|
91
|
-
class Notifications
|
|
92
|
-
def initialize(config)
|
|
93
|
-
@config = config
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def list(unread_only: nil, limit: nil, cursor: nil)
|
|
97
|
-
q = {}
|
|
98
|
-
q['unreadOnly'] = unread_only unless unread_only.nil?
|
|
99
|
-
q['limit'] = limit if limit
|
|
100
|
-
q['cursor'] = cursor unless cursor.nil?
|
|
101
|
-
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/notifications'), query: q)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def mark_read(notification_id)
|
|
105
|
-
HttpTransport.request(config: @config, method: 'PATCH',
|
|
106
|
-
path: UrlGrammar.org_path("me/notifications/#{enc(notification_id)}"), body: { 'read' => true })
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def mark_all_read
|
|
110
|
-
HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('me/notifications'),
|
|
111
|
-
body: { 'read' => true })
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
def enc(s)
|
|
117
|
-
::URI.encode_www_form_component(s)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# k9o.me.audit_log.*
|
|
122
|
-
class AuditLog
|
|
123
|
-
def initialize(config)
|
|
124
|
-
@config = config
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def query(since: nil, until_: nil, type: nil, limit: nil, cursor: nil)
|
|
128
|
-
q = {}
|
|
129
|
-
q['since'] = since if since
|
|
130
|
-
q['until'] = until_ if until_
|
|
131
|
-
q['type'] = type if type
|
|
132
|
-
q['limit'] = limit if limit
|
|
133
|
-
q['cursor'] = cursor if cursor
|
|
134
|
-
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/audit-log'), query: q)
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# k9o.me.data_export.*
|
|
139
|
-
class DataExport
|
|
140
|
-
def initialize(config)
|
|
141
|
-
@config = config
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def request(format: 'json')
|
|
145
|
-
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('me/data-export'),
|
|
146
|
-
body: { 'format' => format })
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def status(job_id)
|
|
150
|
-
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("me/data-export/#{enc(job_id)}"))
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def enc(s)
|
|
156
|
-
::URI.encode_www_form_component(s)
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
99
|
end
|
|
160
100
|
end
|