fripa 0.3.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 +6 -0
- data/README.md +21 -0
- data/lib/fripa/batch.rb +39 -0
- data/lib/fripa/client.rb +2 -10
- data/lib/fripa/errors.rb +25 -0
- data/lib/fripa/resources/vault.rb +29 -12
- data/lib/fripa/version.rb +1 -1
- data/lib/fripa.rb +1 -0
- data/sig/fripa.rbs +12 -0
- 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,12 @@ 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
|
+
|
|
9
15
|
## [0.3.0] - 2026-10-15
|
|
10
16
|
|
|
11
17
|
### Changed
|
data/README.md
CHANGED
|
@@ -273,6 +273,27 @@ result.dig("result", "results").each { |r| puts r["error"] || r["result"] }
|
|
|
273
273
|
```
|
|
274
274
|
|
|
275
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
|
+
```
|
|
276
297
|
|
|
277
298
|
#### Using Raw API Calls
|
|
278
299
|
|
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
|
|
@@ -64,16 +66,6 @@ module Fripa
|
|
|
64
66
|
parse_response(response)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
# Sends several commands in one HTTP request. Each command is
|
|
68
|
-
# [method, args = [], options = {}]. Per-command errors are not raised:
|
|
69
|
-
# check result["result"]["results"][i]["error"].
|
|
70
|
-
def batch(*commands)
|
|
71
|
-
requests = commands.map do |method, args = [], options = {}|
|
|
72
|
-
{ method: method, params: [args, options.merge(version: config.api_version)] }
|
|
73
|
-
end
|
|
74
|
-
call("batch", requests)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
69
|
# Shared by Authenticator, so login and API calls reuse one connection.
|
|
78
70
|
def connection
|
|
79
71
|
@connection ||= Faraday.new(url: config.base_url.to_s, request: request_options) do |f|
|
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.
|
|
@@ -33,20 +33,14 @@ module Fripa
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def add(cn, **scope)
|
|
36
|
-
|
|
37
|
-
call("vault_add_internal", [cn], { ipavaulttype: "standard" }.merge(scope))
|
|
36
|
+
call(*add_command(cn, **scope))
|
|
38
37
|
end
|
|
39
38
|
|
|
40
39
|
# ipa_format: true stores the data like `ipa vault-archive`, so the ipa CLI can read it.
|
|
41
40
|
# The default (false) stores the raw bytes, as in previous versions.
|
|
42
41
|
def archive(cn, data:, ipa_format: false, **scope)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
payload = VaultTransport.wrap(data, transport_cert_der)
|
|
46
|
-
with_transport_cert_refresh do
|
|
47
|
-
call("vault_archive_internal", [cn],
|
|
48
|
-
binary_encode(payload).merge(wrapping_algo: VaultTransport::WRAPPING_ALGO).merge(scope))
|
|
49
|
-
end
|
|
42
|
+
command = archive_command(cn, data:, ipa_format:, **scope)
|
|
43
|
+
with_transport_cert_refresh { call(*command) }
|
|
50
44
|
end
|
|
51
45
|
|
|
52
46
|
# ipa_format: true reads data stored by `ipa vault-archive` (or archive with ipa_format: true).
|
|
@@ -66,13 +60,36 @@ module Fripa
|
|
|
66
60
|
end
|
|
67
61
|
|
|
68
62
|
def add_member(cn, **scope)
|
|
69
|
-
|
|
70
|
-
call("vault_add_member", [cn], scope)
|
|
63
|
+
call(*add_member_command(cn, **scope))
|
|
71
64
|
end
|
|
72
65
|
|
|
73
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)
|
|
73
|
+
validate_identifier!(cn, "cn")
|
|
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)
|
|
74
91
|
validate_identifier!(cn, "cn")
|
|
75
|
-
|
|
92
|
+
["vault_del", [cn], scope]
|
|
76
93
|
end
|
|
77
94
|
|
|
78
95
|
private
|
data/lib/fripa/version.rb
CHANGED
data/lib/fripa.rb
CHANGED
data/sig/fripa.rbs
CHANGED
|
@@ -54,6 +54,13 @@ module Fripa
|
|
|
54
54
|
def initialize: (code: Integer?, message: String?, ?data: untyped) -> void
|
|
55
55
|
end
|
|
56
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
|
+
|
|
57
64
|
class PartialFailureError < ResponseError
|
|
58
65
|
def initialize: (json_hash failed) -> void
|
|
59
66
|
def self.failures: (untyped failed) -> Array[untyped]
|
|
@@ -79,6 +86,7 @@ module Fripa
|
|
|
79
86
|
def authenticator: () -> Authenticator
|
|
80
87
|
def call: (String method, ?Array[untyped] args, ?Hash[Symbol, untyped] options) -> json_hash
|
|
81
88
|
def batch: (*Array[untyped] commands) -> json_hash
|
|
89
|
+
def batch!: (*Array[untyped] commands) -> json_hash
|
|
82
90
|
def connection: () -> untyped
|
|
83
91
|
def referer: () -> String
|
|
84
92
|
end
|
|
@@ -157,6 +165,10 @@ module Fripa
|
|
|
157
165
|
def retrieve: (String cn, ?ipa_format: bool, **untyped scope) -> String?
|
|
158
166
|
def add_member: (String cn, **untyped scope) -> json_hash
|
|
159
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]]
|
|
160
172
|
end
|
|
161
173
|
end
|
|
162
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: []
|