fripa 0.2.0 → 0.4.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 +21 -1
- data/README.md +60 -9
- data/lib/fripa/authenticator.rb +2 -2
- data/lib/fripa/batch.rb +39 -0
- data/lib/fripa/client.rb +29 -15
- data/lib/fripa/configuration.rb +38 -2
- data/lib/fripa/errors.rb +25 -0
- data/lib/fripa/resources/vault.rb +67 -17
- data/lib/fripa/version.rb +1 -1
- data/lib/fripa.rb +1 -0
- data/sig/fripa.rbs +30 -4
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e46b8ee8ebb691b57a6f5f72f9bcb5b577bca894aebaefc694f28e621038a2e
|
|
4
|
+
data.tar.gz: 991e2a655e038c28a19be01f2acf6c1e780504b4adb6279788bfff243e5ee0e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b5bb37ed15a9909fc9e035455dc96dd0204e28b22db211d1a930f16e3f0e93168661a6736d2cbca77cfcab990409684ad7b4e3b4cd9abb2d17e0793bc3cdf64
|
|
7
|
+
data.tar.gz: 6a0f3c1cf130a5b05e621dc07b6199684dcc4ffd22bcdfa6e0a531bd3c757709d40f1530166157c29360bd33dfbcc576211d0b2916148e344e2130988d4848b8
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `Fripa::Client#batch!`: like `batch`, but raises `Fripa::BatchError` (a `Fripa::ResponseError`) when any command failed. `#errors` lists each failed command (index, method, code, name, message); `#code` and `#message` are those of the first one.
|
|
13
|
+
- Vault batch commands: `vault.add_command`, `archive_command`, `add_member_command` and `delete_command` return `[method, args, options]` for `Client#batch`, so many vault operations go in one request. `archive_command` wraps the data with the transport certificate, like `archive`. When a transport command fails in `batch!`, the certificate is forgotten and fetched again on the next call, as with `archive`.
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-10-15
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **`Fripa::Client.new` no longer connects.** The client logs in on the first call. Wrong credentials now raise `Fripa::AuthenticationError` on the first call, not in `Client.new`. Call `client.login!` to log in at once. Blank credentials still raise `ArgumentError` in `Client.new`.
|
|
19
|
+
- The vault transport certificate is cached per process and server URL, so all clients share it (before: one fetch per client). It is fetched again after a vault call fails.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `Fripa.client`: one client for the account in `config.username` / `config.password`, shared by the process. It is built on first use and again when the configuration changes. `Fripa.reset_client!` forgets the shared client (e.g. between tests).
|
|
23
|
+
- `Fripa::Client#login!` (returns the session cookie) and `#logged_in?`.
|
|
24
|
+
- `Fripa::Client.new(username:, session_cookie:)` builds a client from an earlier session, without a login request. `password` is now optional. Without a password, an expired session raises `Fripa::AuthenticationError`.
|
|
25
|
+
- `Fripa::Client.new` without `username` uses the credentials from the configuration. With a `username`, it never uses the configured password.
|
|
26
|
+
- `Configuration#inspect` does not show the password.
|
|
27
|
+
|
|
9
28
|
## [0.2.0] - 2026-10-12
|
|
10
29
|
|
|
11
30
|
### Changed
|
|
@@ -59,7 +78,8 @@ Initial release of Fripa - A Ruby client for the FreeIPA JSON-RPC API.
|
|
|
59
78
|
- Full test suite with VCR cassettes
|
|
60
79
|
- Complete API documentation and usage examples
|
|
61
80
|
|
|
62
|
-
[Unreleased]: https://github.com/renuo/fripa/compare/v0.
|
|
81
|
+
[Unreleased]: https://github.com/renuo/fripa/compare/v0.3.0...HEAD
|
|
82
|
+
[0.3.0]: https://github.com/renuo/fripa/compare/v0.2.0...v0.3.0
|
|
63
83
|
[0.2.0]: https://github.com/renuo/fripa/compare/v0.1.4...v0.2.0
|
|
64
84
|
[0.1.4]: https://github.com/renuo/fripa/compare/v0.1.3...v0.1.4
|
|
65
85
|
[0.1.3]: https://github.com/renuo/fripa/compare/v0.1.2...v0.1.3
|
data/README.md
CHANGED
|
@@ -114,26 +114,54 @@ end
|
|
|
114
114
|
|
|
115
115
|
### Creating a Client
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
#### Service account (shared client)
|
|
118
|
+
|
|
119
|
+
Put the credentials in the configuration and use `Fripa.client`. It is one
|
|
120
|
+
client for the whole process: it logs in on the first call, keeps the session,
|
|
121
|
+
and logs in again when the session expires. It is built again when the
|
|
122
|
+
configuration changes.
|
|
118
123
|
|
|
119
124
|
```ruby
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
Fripa.configure do |config|
|
|
126
|
+
config.host = 'ipa.example.com'
|
|
127
|
+
config.username = 'admin'
|
|
128
|
+
config.password = 'secret'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Fripa.client.users.show('alice')
|
|
125
132
|
```
|
|
126
133
|
|
|
127
|
-
|
|
134
|
+
In tests, call `Fripa.reset_client!` before each test, so no session or test
|
|
135
|
+
double stays from one test to the next.
|
|
136
|
+
|
|
137
|
+
#### Other accounts
|
|
138
|
+
|
|
139
|
+
`Fripa::Client.new` creates a client for one account. It does not connect: it
|
|
140
|
+
logs in on the first call. Call `login!` to log in now, for example to check
|
|
141
|
+
credentials. It returns the session cookie.
|
|
128
142
|
|
|
129
143
|
```ruby
|
|
144
|
+
client = Fripa::Client.new(username: 'alice', password: 'her-password')
|
|
145
|
+
|
|
130
146
|
begin
|
|
131
|
-
|
|
147
|
+
cookie = client.login!
|
|
132
148
|
rescue Fripa::AuthenticationError => e
|
|
133
149
|
puts "Authentication failed: #{e.message}"
|
|
134
150
|
end
|
|
135
151
|
```
|
|
136
152
|
|
|
153
|
+
To skip the login in later requests, build the client from the session cookie
|
|
154
|
+
(e.g. kept in an encrypted session):
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
client = Fripa::Client.new(username: 'alice', session_cookie: cookie)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A client without a password cannot log in again: when its session expires, it
|
|
161
|
+
raises `Fripa::AuthenticationError`. With a `username`, `Fripa::Client.new`
|
|
162
|
+
never uses the password from the configuration. A client can be shared between
|
|
163
|
+
threads.
|
|
164
|
+
|
|
137
165
|
### Making API Calls
|
|
138
166
|
|
|
139
167
|
#### Using Resource Methods (Recommended)
|
|
@@ -228,7 +256,9 @@ client.vault.archive("my-vault", data: "secret", ipa_format: true, shared: true)
|
|
|
228
256
|
client.vault.retrieve("my-vault", ipa_format: true, shared: true) # => "secret"
|
|
229
257
|
```
|
|
230
258
|
|
|
231
|
-
The
|
|
259
|
+
The KRA transport certificate is fetched once per process and server, and all
|
|
260
|
+
clients share it (it is public and the same for every account). If the server
|
|
261
|
+
rejects a vault call, the certificate is fetched again on the next call.
|
|
232
262
|
|
|
233
263
|
#### Batch Calls
|
|
234
264
|
|
|
@@ -243,6 +273,27 @@ result.dig("result", "results").each { |r| puts r["error"] || r["result"] }
|
|
|
243
273
|
```
|
|
244
274
|
|
|
245
275
|
`batch` does not raise for errors in a single command. Check `error` in each result.
|
|
276
|
+
`batch!` raises `Fripa::BatchError` when any command failed. `#errors` lists them, with
|
|
277
|
+
the index, method, code, name and message of each:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
client.batch!(["vault_del", ["v1"]], ["vault_del", ["v2"]])
|
|
281
|
+
rescue Fripa::BatchError => e
|
|
282
|
+
e.errors # => [{ index: 1, method: "vault_del", code: 4001, name: "NotFound", message: "v2: vault not found" }]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The vault resource builds batch commands with `add_command`, `archive_command`,
|
|
286
|
+
`add_member_command` and `delete_command`. They take the same arguments as `add`,
|
|
287
|
+
`archive`, `add_member` and `delete`, so you can set up many vaults in one request:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
vault = client.vault
|
|
291
|
+
client.batch!(*%w[v1 v2].flat_map do |cn|
|
|
292
|
+
[vault.add_command(cn, shared: true),
|
|
293
|
+
vault.archive_command(cn, data: "secret", shared: true),
|
|
294
|
+
vault.add_member_command(cn, user: ["alice"], shared: true)]
|
|
295
|
+
end)
|
|
296
|
+
```
|
|
246
297
|
|
|
247
298
|
#### Using Raw API Calls
|
|
248
299
|
|
data/lib/fripa/authenticator.rb
CHANGED
|
@@ -30,13 +30,13 @@ module Fripa
|
|
|
30
30
|
"#<#{self.class.name} username=#{client.username.inspect}>"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
private
|
|
34
|
-
|
|
35
33
|
def validate_credentials!
|
|
36
34
|
raise ArgumentError, "Username cannot be blank" if client.username.to_s.strip.empty?
|
|
37
35
|
raise ArgumentError, "Password cannot be blank" if client.password.to_s.strip.empty?
|
|
38
36
|
end
|
|
39
37
|
|
|
38
|
+
private
|
|
39
|
+
|
|
40
40
|
def perform_login
|
|
41
41
|
client.connection.post(LOGIN_PATH) do |request|
|
|
42
42
|
request.headers["Content-Type"] = CONTENT_TYPE
|
data/lib/fripa/batch.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fripa
|
|
4
|
+
# Batch calls for Fripa::Client: many commands in one HTTP request.
|
|
5
|
+
module Batch
|
|
6
|
+
# Sends several commands in one HTTP request. Each command is
|
|
7
|
+
# [method, args = [], options = {}]. Per-command errors are not raised:
|
|
8
|
+
# check result["result"]["results"][i]["error"].
|
|
9
|
+
def batch(*commands)
|
|
10
|
+
requests = commands.map do |method, args = [], options = {}|
|
|
11
|
+
{ method: method, params: [args, options.merge(version: config.api_version)] }
|
|
12
|
+
end
|
|
13
|
+
call("batch", requests)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Commands wrapped with the vault transport certificate (see Resources::Vault#archive_command).
|
|
17
|
+
TRANSPORT_COMMANDS = %w[vault_archive_internal vault_retrieve_internal].freeze
|
|
18
|
+
|
|
19
|
+
# Like batch, but raises Fripa::BatchError when any command failed.
|
|
20
|
+
# Like Resources::Vault#archive, a failed transport command forgets the certificate, so the next
|
|
21
|
+
# call fetches it again (e.g. after the server renewed it).
|
|
22
|
+
def batch!(*commands)
|
|
23
|
+
batch(*commands).tap { |response| BatchError.check!(commands, response) }
|
|
24
|
+
rescue BatchError => e
|
|
25
|
+
forget_transport_cert_after(e)
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def forget_transport_cert_after(error)
|
|
32
|
+
return unless error.errors.any? do |failed|
|
|
33
|
+
TRANSPORT_COMMANDS.include?(failed[:method]) && failed[:code] != Resources::Vault::NOT_FOUND
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Resources::Vault.forget_transport_cert(config.base_url.to_s)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/fripa/client.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "json"
|
|
|
5
5
|
|
|
6
6
|
module Fripa
|
|
7
7
|
class Client
|
|
8
|
+
include Batch
|
|
9
|
+
|
|
8
10
|
API_PATH = "/ipa/session/json"
|
|
9
11
|
API_VERSION = Configuration::DEFAULT_API_VERSION
|
|
10
12
|
UNAUTHORIZED = 401
|
|
@@ -12,11 +14,18 @@ module Fripa
|
|
|
12
14
|
attr_reader :config, :username, :password
|
|
13
15
|
attr_accessor :session_cookie
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
# Does not connect: the client logs in on the first call, or when you call login!.
|
|
18
|
+
# Without username, it uses config.username and config.password. With a
|
|
19
|
+
# username, it never uses the password from the configuration.
|
|
20
|
+
# Give a session_cookie from an earlier login to skip the login request.
|
|
21
|
+
# Without a password, an expired session raises AuthenticationError.
|
|
22
|
+
def initialize(username: nil, password: nil, config: nil, session_cookie: nil)
|
|
16
23
|
@config = config || Fripa.config
|
|
17
|
-
@username = username
|
|
18
|
-
@password = password
|
|
19
|
-
@session_cookie =
|
|
24
|
+
@username = username || @config.username
|
|
25
|
+
@password = username ? password : password || @config.password
|
|
26
|
+
@session_cookie = session_cookie unless session_cookie.to_s.empty?
|
|
27
|
+
@logged_in = !@session_cookie.nil?
|
|
28
|
+
authenticator.validate_credentials! unless @logged_in
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def users
|
|
@@ -35,7 +44,20 @@ module Fripa
|
|
|
35
44
|
@authenticator ||= Authenticator.new(self)
|
|
36
45
|
end
|
|
37
46
|
|
|
47
|
+
# Logs in now and returns the session cookie. Raises AuthenticationError
|
|
48
|
+
# if the credentials are wrong.
|
|
49
|
+
def login!
|
|
50
|
+
self.session_cookie = authenticator.login!
|
|
51
|
+
@logged_in = true
|
|
52
|
+
session_cookie
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def logged_in?
|
|
56
|
+
@logged_in
|
|
57
|
+
end
|
|
58
|
+
|
|
38
59
|
def call(method, args = [], options = {})
|
|
60
|
+
login! unless logged_in?
|
|
39
61
|
response = perform_request(method, args, options)
|
|
40
62
|
response = reauthenticate_and_retry(method, args, options) if response.status == UNAUTHORIZED
|
|
41
63
|
|
|
@@ -44,16 +66,6 @@ module Fripa
|
|
|
44
66
|
parse_response(response)
|
|
45
67
|
end
|
|
46
68
|
|
|
47
|
-
# Sends several commands in one HTTP request. Each command is
|
|
48
|
-
# [method, args = [], options = {}]. Per-command errors are not raised:
|
|
49
|
-
# check result["result"]["results"][i]["error"].
|
|
50
|
-
def batch(*commands)
|
|
51
|
-
requests = commands.map do |method, args = [], options = {}|
|
|
52
|
-
{ method: method, params: [args, options.merge(version: config.api_version)] }
|
|
53
|
-
end
|
|
54
|
-
call("batch", requests)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
69
|
# Shared by Authenticator, so login and API calls reuse one connection.
|
|
58
70
|
def connection
|
|
59
71
|
@connection ||= Faraday.new(url: config.base_url.to_s, request: request_options) do |f|
|
|
@@ -73,7 +85,9 @@ module Fripa
|
|
|
73
85
|
private
|
|
74
86
|
|
|
75
87
|
def reauthenticate_and_retry(method, args, options)
|
|
76
|
-
|
|
88
|
+
raise AuthenticationError, "Session expired and no password to log in again" if password.to_s.empty?
|
|
89
|
+
|
|
90
|
+
login!
|
|
77
91
|
perform_request(method, args, options)
|
|
78
92
|
end
|
|
79
93
|
|
data/lib/fripa/configuration.rb
CHANGED
|
@@ -10,10 +10,13 @@ module Fripa
|
|
|
10
10
|
SCHEMES = %w[http https].freeze
|
|
11
11
|
|
|
12
12
|
attr_accessor :host, :port, :scheme, :verify_ssl, :api_version, :timeout, :open_timeout, :adapter,
|
|
13
|
-
:raise_on_partial_failure
|
|
13
|
+
:raise_on_partial_failure, :username, :password
|
|
14
14
|
|
|
15
|
+
# username and password are only used by Fripa.client and by
|
|
16
|
+
# Fripa::Client.new without a username.
|
|
15
17
|
def initialize(host: nil, port: nil, scheme: "https", verify_ssl: true, api_version: DEFAULT_API_VERSION,
|
|
16
|
-
timeout: nil, open_timeout: nil, adapter: nil, raise_on_partial_failure: false
|
|
18
|
+
timeout: nil, open_timeout: nil, adapter: nil, raise_on_partial_failure: false,
|
|
19
|
+
username: nil, password: nil)
|
|
17
20
|
@host = host
|
|
18
21
|
@port = port
|
|
19
22
|
@scheme = scheme
|
|
@@ -23,6 +26,19 @@ module Fripa
|
|
|
23
26
|
@open_timeout = open_timeout
|
|
24
27
|
@adapter = adapter
|
|
25
28
|
@raise_on_partial_failure = raise_on_partial_failure
|
|
29
|
+
@username = username
|
|
30
|
+
@password = password
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Values that change how a client connects or logs in. Fripa.client builds
|
|
34
|
+
# a new client when they change.
|
|
35
|
+
def options
|
|
36
|
+
{ host:, port:, scheme:, verify_ssl:, api_version:, timeout:, open_timeout:, adapter:,
|
|
37
|
+
raise_on_partial_failure:, username:, password: }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<#{self.class.name} host=#{host.inspect} scheme=#{scheme.inspect} username=#{username.inspect}>"
|
|
26
42
|
end
|
|
27
43
|
|
|
28
44
|
def base_url
|
|
@@ -50,11 +66,31 @@ module Fripa
|
|
|
50
66
|
end
|
|
51
67
|
end
|
|
52
68
|
|
|
69
|
+
@client_lock = Mutex.new
|
|
70
|
+
|
|
53
71
|
class << self
|
|
54
72
|
def config
|
|
55
73
|
@config ||= Configuration.new
|
|
56
74
|
end
|
|
57
75
|
|
|
76
|
+
# A client for the account in Fripa.config (username and password), shared
|
|
77
|
+
# by the whole process. It is built on first use, and again when the
|
|
78
|
+
# configuration changes. Use Fripa::Client.new for other accounts.
|
|
79
|
+
def client
|
|
80
|
+
@client_lock.synchronize do
|
|
81
|
+
unless @client&.config.equal?(config) && @client_options == config.options
|
|
82
|
+
@client = Client.new(config: config)
|
|
83
|
+
@client_options = config.options
|
|
84
|
+
end
|
|
85
|
+
@client
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Forgets the shared client (e.g. between tests). The next Fripa.client builds a new one.
|
|
90
|
+
def reset_client!
|
|
91
|
+
@client_lock.synchronize { @client = @client_options = nil }
|
|
92
|
+
end
|
|
93
|
+
|
|
58
94
|
def config=(value)
|
|
59
95
|
@config = case value
|
|
60
96
|
when Hash
|
data/lib/fripa/errors.rb
CHANGED
|
@@ -15,6 +15,31 @@ module Fripa
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Raised by Client#batch! when one or more commands of the batch failed. #errors lists them:
|
|
19
|
+
# { index:, method:, code:, name:, message: }. #code and #message are those of the first one.
|
|
20
|
+
# #data holds every result of the batch.
|
|
21
|
+
class BatchError < ResponseError
|
|
22
|
+
attr_reader :errors
|
|
23
|
+
|
|
24
|
+
def initialize(errors, results)
|
|
25
|
+
@errors = errors
|
|
26
|
+
first = errors.first
|
|
27
|
+
super(code: first[:code], message: "#{first[:method]}: #{first[:message]}", data: results)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Raises a BatchError if any result of the batch response holds an error.
|
|
31
|
+
def self.check!(commands, response)
|
|
32
|
+
results = response.dig("result", "results") || []
|
|
33
|
+
errors = results.each_with_index.filter_map do |result, index|
|
|
34
|
+
next unless result["error"]
|
|
35
|
+
|
|
36
|
+
{ index: index, method: commands[index].first, code: result["error_code"],
|
|
37
|
+
name: result["error_name"], message: result["error"] }
|
|
38
|
+
end
|
|
39
|
+
raise new(errors, results) if errors.any?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
18
43
|
# Raised when FreeIPA answers without an error but reports per-entry failures,
|
|
19
44
|
# e.g. group_add_member with a user that does not exist or is already a member.
|
|
20
45
|
# Only raised with config.raise_on_partial_failure = true.
|
|
@@ -5,24 +5,42 @@ require "base64"
|
|
|
5
5
|
module Fripa
|
|
6
6
|
module Resources
|
|
7
7
|
class Vault < Base
|
|
8
|
+
NOT_FOUND = 4001
|
|
9
|
+
|
|
10
|
+
# The KRA transport certificate belongs to the server, not to an account,
|
|
11
|
+
# and it is public. So all clients of one process share it, per server URL:
|
|
12
|
+
# a service account and per-user clients fetch it only once.
|
|
13
|
+
@transport_certs = {}
|
|
14
|
+
@transport_certs_lock = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def transport_cert(key)
|
|
18
|
+
@transport_certs_lock.synchronize { @transport_certs[key] ||= yield }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def forget_transport_cert(key)
|
|
22
|
+
@transport_certs_lock.synchronize { @transport_certs.delete(key) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def clear_transport_certs!
|
|
26
|
+
@transport_certs_lock.synchronize { @transport_certs.clear }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
8
30
|
def show(cn, **scope)
|
|
9
31
|
validate_identifier!(cn, "cn")
|
|
10
32
|
call("vault_show", [cn], scope)
|
|
11
33
|
end
|
|
12
34
|
|
|
13
35
|
def add(cn, **scope)
|
|
14
|
-
|
|
15
|
-
call("vault_add_internal", [cn], { ipavaulttype: "standard" }.merge(scope))
|
|
36
|
+
call(*add_command(cn, **scope))
|
|
16
37
|
end
|
|
17
38
|
|
|
18
39
|
# ipa_format: true stores the data like `ipa vault-archive`, so the ipa CLI can read it.
|
|
19
40
|
# The default (false) stores the raw bytes, as in previous versions.
|
|
20
41
|
def archive(cn, data:, ipa_format: false, **scope)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
payload = VaultTransport.wrap(data, transport_cert_der)
|
|
24
|
-
call("vault_archive_internal", [cn],
|
|
25
|
-
binary_encode(payload).merge(wrapping_algo: VaultTransport::WRAPPING_ALGO).merge(scope))
|
|
42
|
+
command = archive_command(cn, data:, ipa_format:, **scope)
|
|
43
|
+
with_transport_cert_refresh { call(*command) }
|
|
26
44
|
end
|
|
27
45
|
|
|
28
46
|
# ipa_format: true reads data stored by `ipa vault-archive` (or archive with ipa_format: true).
|
|
@@ -42,21 +60,51 @@ module Fripa
|
|
|
42
60
|
end
|
|
43
61
|
|
|
44
62
|
def add_member(cn, **scope)
|
|
45
|
-
|
|
46
|
-
call("vault_add_member", [cn], scope)
|
|
63
|
+
call(*add_member_command(cn, **scope))
|
|
47
64
|
end
|
|
48
65
|
|
|
49
66
|
def delete(cn, **scope)
|
|
67
|
+
call(*delete_command(cn, **scope))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The *_command methods build [method, args, options] for Client#batch, so many vault
|
|
71
|
+
# operations go in one request. archive_command wraps the data like archive does.
|
|
72
|
+
def add_command(cn, **scope)
|
|
50
73
|
validate_identifier!(cn, "cn")
|
|
51
|
-
|
|
74
|
+
["vault_add_internal", [cn], { ipavaulttype: "standard" }.merge(scope)]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def archive_command(cn, data:, ipa_format: false, **scope)
|
|
78
|
+
validate_identifier!(cn, "cn")
|
|
79
|
+
data = VaultTransport.to_ipa_format(data) if ipa_format
|
|
80
|
+
payload = VaultTransport.wrap(data, transport_cert_der)
|
|
81
|
+
["vault_archive_internal", [cn],
|
|
82
|
+
binary_encode(payload).merge(wrapping_algo: VaultTransport::WRAPPING_ALGO).merge(scope)]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def add_member_command(cn, **scope)
|
|
86
|
+
validate_identifier!(cn, "cn")
|
|
87
|
+
["vault_add_member", [cn], scope]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def delete_command(cn, **scope)
|
|
91
|
+
validate_identifier!(cn, "cn")
|
|
92
|
+
["vault_del", [cn], scope]
|
|
52
93
|
end
|
|
53
94
|
|
|
54
95
|
private
|
|
55
96
|
|
|
56
|
-
# The KRA transport certificate does not change between calls, so we
|
|
57
|
-
# fetch it once per client (the ipa CLI caches it too).
|
|
58
97
|
def transport_cert_der
|
|
59
|
-
|
|
98
|
+
self.class.transport_cert(client.config.base_url.to_s) { fetch_transport_cert }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# If the server rejects a payload, its certificate can have changed
|
|
102
|
+
# (e.g. renewed). Forget it, so that the next call fetches it again.
|
|
103
|
+
def with_transport_cert_refresh
|
|
104
|
+
yield
|
|
105
|
+
rescue ResponseError => e
|
|
106
|
+
self.class.forget_transport_cert(client.config.base_url.to_s) unless e.code == NOT_FOUND
|
|
107
|
+
raise
|
|
60
108
|
end
|
|
61
109
|
|
|
62
110
|
def fetch_transport_cert
|
|
@@ -67,10 +115,12 @@ module Fripa
|
|
|
67
115
|
def call_retrieve(cn, session_key, **scope)
|
|
68
116
|
encrypted_key = VaultTransport.encrypt_session_key(session_key, transport_cert_der)
|
|
69
117
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
118
|
+
with_transport_cert_refresh do
|
|
119
|
+
call("vault_retrieve_internal", [cn], {
|
|
120
|
+
session_key: { "__base64__" => Base64.strict_encode64(encrypted_key) },
|
|
121
|
+
wrapping_algo: VaultTransport::WRAPPING_ALGO
|
|
122
|
+
}.merge(scope))
|
|
123
|
+
end
|
|
74
124
|
end
|
|
75
125
|
|
|
76
126
|
def binary_encode(hash)
|
data/lib/fripa/version.rb
CHANGED
data/lib/fripa.rb
CHANGED
data/sig/fripa.rbs
CHANGED
|
@@ -6,6 +6,8 @@ module Fripa
|
|
|
6
6
|
def self.config: () -> Configuration
|
|
7
7
|
def self.config=: (Configuration | Hash[Symbol, untyped] value) -> Configuration
|
|
8
8
|
def self.configure: () { (Configuration) -> void } -> void
|
|
9
|
+
def self.client: () -> Client
|
|
10
|
+
def self.reset_client!: () -> void
|
|
9
11
|
|
|
10
12
|
class Configuration
|
|
11
13
|
DEFAULT_API_VERSION: String
|
|
@@ -20,10 +22,13 @@ module Fripa
|
|
|
20
22
|
attr_accessor open_timeout: Numeric?
|
|
21
23
|
attr_accessor adapter: untyped
|
|
22
24
|
attr_accessor raise_on_partial_failure: bool
|
|
25
|
+
attr_accessor username: String?
|
|
26
|
+
attr_accessor password: String?
|
|
23
27
|
|
|
24
28
|
def initialize: (?host: String?, ?port: Integer?, ?scheme: String, ?verify_ssl: bool, ?api_version: String,
|
|
25
29
|
?timeout: Numeric?, ?open_timeout: Numeric?, ?adapter: untyped,
|
|
26
|
-
?raise_on_partial_failure: bool) -> void
|
|
30
|
+
?raise_on_partial_failure: bool, ?username: String?, ?password: String?) -> void
|
|
31
|
+
def options: () -> Hash[Symbol, untyped]
|
|
27
32
|
def base_url: () -> URI::Generic
|
|
28
33
|
def login_url: () -> URI::Generic
|
|
29
34
|
def api_url: () -> URI::Generic
|
|
@@ -49,6 +54,13 @@ module Fripa
|
|
|
49
54
|
def initialize: (code: Integer?, message: String?, ?data: untyped) -> void
|
|
50
55
|
end
|
|
51
56
|
|
|
57
|
+
class BatchError < ResponseError
|
|
58
|
+
attr_reader errors: Array[Hash[Symbol, untyped]]
|
|
59
|
+
|
|
60
|
+
def initialize: (Array[Hash[Symbol, untyped]] errors, Array[json_hash] results) -> void
|
|
61
|
+
def self.check!: (Array[Array[untyped]] commands, json_hash response) -> void
|
|
62
|
+
end
|
|
63
|
+
|
|
52
64
|
class PartialFailureError < ResponseError
|
|
53
65
|
def initialize: (json_hash failed) -> void
|
|
54
66
|
def self.failures: (untyped failed) -> Array[untyped]
|
|
@@ -61,17 +73,20 @@ module Fripa
|
|
|
61
73
|
UNAUTHORIZED: Integer
|
|
62
74
|
|
|
63
75
|
attr_reader config: Configuration
|
|
64
|
-
attr_reader username: String
|
|
65
|
-
attr_reader password: String
|
|
76
|
+
attr_reader username: String?
|
|
77
|
+
attr_reader password: String?
|
|
66
78
|
attr_accessor session_cookie: String?
|
|
67
79
|
|
|
68
|
-
def initialize: (username: String
|
|
80
|
+
def initialize: (?username: String?, ?password: String?, ?config: Configuration?, ?session_cookie: String?) -> void
|
|
81
|
+
def login!: () -> String?
|
|
82
|
+
def logged_in?: () -> bool
|
|
69
83
|
def users: () -> Resources::User
|
|
70
84
|
def groups: () -> Resources::Group
|
|
71
85
|
def vault: () -> Resources::Vault
|
|
72
86
|
def authenticator: () -> Authenticator
|
|
73
87
|
def call: (String method, ?Array[untyped] args, ?Hash[Symbol, untyped] options) -> json_hash
|
|
74
88
|
def batch: (*Array[untyped] commands) -> json_hash
|
|
89
|
+
def batch!: (*Array[untyped] commands) -> json_hash
|
|
75
90
|
def connection: () -> untyped
|
|
76
91
|
def referer: () -> String
|
|
77
92
|
end
|
|
@@ -85,6 +100,7 @@ module Fripa
|
|
|
85
100
|
|
|
86
101
|
def initialize: (Client client) -> void
|
|
87
102
|
def login!: () -> String?
|
|
103
|
+
def validate_credentials!: () -> void
|
|
88
104
|
end
|
|
89
105
|
|
|
90
106
|
module VaultTransport
|
|
@@ -137,12 +153,22 @@ module Fripa
|
|
|
137
153
|
end
|
|
138
154
|
|
|
139
155
|
class Vault < Base
|
|
156
|
+
NOT_FOUND: Integer
|
|
157
|
+
|
|
158
|
+
def self.transport_cert: (String key) { () -> String } -> String
|
|
159
|
+
def self.forget_transport_cert: (String key) -> String?
|
|
160
|
+
def self.clear_transport_certs!: () -> void
|
|
161
|
+
|
|
140
162
|
def show: (String cn, **untyped scope) -> json_hash
|
|
141
163
|
def add: (String cn, **untyped scope) -> json_hash
|
|
142
164
|
def archive: (String cn, data: String, ?ipa_format: bool, **untyped scope) -> json_hash
|
|
143
165
|
def retrieve: (String cn, ?ipa_format: bool, **untyped scope) -> String?
|
|
144
166
|
def add_member: (String cn, **untyped scope) -> json_hash
|
|
145
167
|
def delete: (String cn, **untyped scope) -> json_hash
|
|
168
|
+
def add_command: (String cn, **untyped scope) -> [String, Array[String], Hash[Symbol, untyped]]
|
|
169
|
+
def archive_command: (String cn, data: String, ?ipa_format: bool, **untyped scope) -> [String, Array[String], Hash[Symbol, untyped]]
|
|
170
|
+
def add_member_command: (String cn, **untyped scope) -> [String, Array[String], Hash[Symbol, untyped]]
|
|
171
|
+
def delete_command: (String cn, **untyped scope) -> [String, Array[String], Hash[Symbol, untyped]]
|
|
146
172
|
end
|
|
147
173
|
end
|
|
148
174
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fripa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Costanzo
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- README.md
|
|
51
51
|
- lib/fripa.rb
|
|
52
52
|
- lib/fripa/authenticator.rb
|
|
53
|
+
- lib/fripa/batch.rb
|
|
53
54
|
- lib/fripa/client.rb
|
|
54
55
|
- lib/fripa/configuration.rb
|
|
55
56
|
- lib/fripa/errors.rb
|
|
@@ -83,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
84
|
- !ruby/object:Gem::Version
|
|
84
85
|
version: '0'
|
|
85
86
|
requirements: []
|
|
86
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.20
|
|
87
88
|
specification_version: 4
|
|
88
89
|
summary: Ruby client for FreeIPA JSON-RPC API
|
|
89
90
|
test_files: []
|