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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ac5a037cb023bb2f087a6d3f6482c4ca8fa8a1e7f7b70d7a01d88e8e7e62db8
4
- data.tar.gz: a7f6e6894ee51a0195f4555e482e29752188805c0c908791232bc8fa7db272f0
3
+ metadata.gz: 0e46b8ee8ebb691b57a6f5f72f9bcb5b577bca894aebaefc694f28e621038a2e
4
+ data.tar.gz: 991e2a655e038c28a19be01f2acf6c1e780504b4adb6279788bfff243e5ee0e1
5
5
  SHA512:
6
- metadata.gz: be1d590702cd1e50e0995b6e8b65683c691ec0f03522ff755126645566d7f87cbd7be9c311db970900ee6b28a3931a5563cadd7cae1c9d24d3982b6bea94c3c5
7
- data.tar.gz: 9acd2a8785b03cabf3a4902db60efdc6e5c895137759c8371ecf7577ce94f8c37daab75631ed81ea4918cfe795ebf74e23af716fcf3bfbfa3fdde4bd2b3e19d3
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
 
@@ -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
- validate_identifier!(cn, "cn")
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
- validate_identifier!(cn, "cn")
44
- data = VaultTransport.to_ipa_format(data) if ipa_format
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
- validate_identifier!(cn, "cn")
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
- call("vault_del", [cn], scope)
92
+ ["vault_del", [cn], scope]
76
93
  end
77
94
 
78
95
  private
data/lib/fripa/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fripa
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/fripa.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "fripa/resources/user"
9
9
  require_relative "fripa/resources/group"
10
10
  require_relative "fripa/vault_transport"
11
11
  require_relative "fripa/resources/vault"
12
+ require_relative "fripa/batch"
12
13
  require_relative "fripa/client"
13
14
 
14
15
  module Fripa
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.3.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.10
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: []