fripa 0.1.4 → 0.2.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 +45 -0
- data/README.md +88 -1
- data/lib/fripa/authenticator.rb +24 -10
- data/lib/fripa/client.rb +51 -16
- data/lib/fripa/configuration.rb +17 -4
- data/lib/fripa/errors.rb +23 -0
- data/lib/fripa/resources/user.rb +5 -3
- data/lib/fripa/resources/vault.rb +21 -11
- data/lib/fripa/vault_transport.rb +11 -5
- data/lib/fripa/version.rb +1 -1
- data/sig/fripa.rbs +145 -1
- metadata +12 -14
- data/.env.example +0 -4
- data/.simplecov +0 -7
- data/CODE_OF_CONDUCT.md +0 -132
- data/CONTRIBUTING.md +0 -96
- data/Rakefile +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5034dad8f9b68de329b79f4a447167c68f60edbf9310854f587a14b780f1c3f7
|
|
4
|
+
data.tar.gz: 9dc1b84130c58a48f67aeaa103c39893235c28123b38214acde4eefd40509762
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81e82e52a5d09927d3a69ecf550db4482a282faa7fee27b41fcea21d45d27c6764bd7aa45550a21f584ba11c6887b9a3e63829d7c199ca541be1757a7423352a
|
|
7
|
+
data.tar.gz: d95654a1a20db0c69f64ba9f007dd3074011b3eb685b561bf2c2cc6d1ebe1fd8056d0e655073bf4ed2de4e772193238081238049a9f7fc77c5439a74293c6dfa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
1
7
|
## [Unreleased]
|
|
2
8
|
|
|
9
|
+
## [0.2.0] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Network errors and timeouts (`Faraday::Error`) now raise `Fripa::ConnectionError`. Responses that are not JSON also raise `Fripa::ConnectionError`.
|
|
13
|
+
- The session cookie keeps only `name=value` pairs. Attributes like `path`, `httponly` and `secure` are no longer sent back.
|
|
14
|
+
- Login and API calls share one HTTP connection.
|
|
15
|
+
- `Fripa::Client#inspect` and `Fripa::Authenticator#inspect` no longer show the password or the session cookie.
|
|
16
|
+
- `scheme` is case-insensitive. An unknown scheme raises `ArgumentError` instead of falling back to HTTP.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `config.raise_on_partial_failure` (default `false`): when `true`, calls that report per-entry failures (e.g. `group_add_member` with an unknown user, or a user that is already a member) raise `Fripa::PartialFailureError` (a `Fripa::ResponseError`). The full `failed` hash is in `#data`.
|
|
20
|
+
- The client logs in again and retries once when the session expires (HTTP 401).
|
|
21
|
+
- `Fripa::Client#batch` sends many commands in one HTTP request.
|
|
22
|
+
- Configuration options: `timeout`, `open_timeout`, `api_version` and `adapter` (e.g. `:net_http_persistent` for keep-alive).
|
|
23
|
+
- `ipa_format:` option for `vault.archive` and `vault.retrieve`, to read and write vaults in the format of the `ipa` CLI.
|
|
24
|
+
- `users.passwd` accepts no current password, so admins can reset the password of another user.
|
|
25
|
+
- RBS signatures in `sig/`.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `vault.retrieve` returns `nil` for an empty vault instead of raising `NoMethodError`.
|
|
29
|
+
- The vault resource fetches the KRA transport certificate once per client, not on every archive and retrieve.
|
|
30
|
+
|
|
31
|
+
## [0.1.4] - 2026-04-16
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- Vault resource: `add`, `show`, `archive`, `retrieve`, `add_member`, `delete`, with transport encryption (AES-128-CBC session key wrapped with the KRA transport certificate).
|
|
35
|
+
|
|
36
|
+
## [0.1.3] - 2025-11-26
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- `Fripa::ResponseError` with `#code`, `#message` and `#data` for errors returned by FreeIPA.
|
|
40
|
+
|
|
3
41
|
## [0.1.2] - 2025-11-14
|
|
4
42
|
|
|
5
43
|
### Fixed
|
|
@@ -20,3 +58,10 @@ Initial release of Fripa - A Ruby client for the FreeIPA JSON-RPC API.
|
|
|
20
58
|
- Parameter validation for resource methods
|
|
21
59
|
- Full test suite with VCR cassettes
|
|
22
60
|
- Complete API documentation and usage examples
|
|
61
|
+
|
|
62
|
+
[Unreleased]: https://github.com/renuo/fripa/compare/v0.2.0...HEAD
|
|
63
|
+
[0.2.0]: https://github.com/renuo/fripa/compare/v0.1.4...v0.2.0
|
|
64
|
+
[0.1.4]: https://github.com/renuo/fripa/compare/v0.1.3...v0.1.4
|
|
65
|
+
[0.1.3]: https://github.com/renuo/fripa/compare/v0.1.2...v0.1.3
|
|
66
|
+
[0.1.2]: https://github.com/renuo/fripa/compare/v0.1.0...v0.1.2
|
|
67
|
+
[0.1.0]: https://github.com/renuo/fripa/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Fripa
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/fripa)
|
|
4
|
+
[](https://github.com/renuo/fripa/actions/workflows/main.yml)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](https://rubydoc.info/gems/fripa)
|
|
7
|
+
[](LICENSE.txt)
|
|
8
|
+
|
|
3
9
|
A Ruby client for the FreeIPA JSON-RPC API.
|
|
4
10
|
|
|
5
11
|
Docs: [FreeIPA JSON-RPC API](https://freeipa.readthedocs.io/en/latest/api/json-rpc.html)
|
|
@@ -74,6 +80,23 @@ Fripa.config.scheme = 'https' # optional
|
|
|
74
80
|
Fripa.config.verify_ssl = false
|
|
75
81
|
```
|
|
76
82
|
|
|
83
|
+
### Advanced options
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
Fripa.configure do |config|
|
|
87
|
+
config.timeout = 30 # read timeout in seconds (default: Faraday default)
|
|
88
|
+
config.open_timeout = 5 # connect timeout in seconds (default: Faraday default)
|
|
89
|
+
config.api_version = "2.251" # FreeIPA API version sent with each call
|
|
90
|
+
config.adapter = :net_http_persistent # any Faraday adapter (default: Faraday.default_adapter)
|
|
91
|
+
config.raise_on_partial_failure = true # raise Fripa::PartialFailureError (default: false)
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The default adapter opens a new TCP/TLS connection for every request. If you
|
|
96
|
+
make many calls with one client, a keep-alive adapter is much faster. Add
|
|
97
|
+
`gem "faraday-net_http_persistent"` to your Gemfile and set
|
|
98
|
+
`config.adapter = :net_http_persistent`.
|
|
99
|
+
|
|
77
100
|
### Local development example
|
|
78
101
|
|
|
79
102
|
For local FreeIPA instances running on HTTP:
|
|
@@ -143,6 +166,9 @@ client.users.mod("newuser", mail: "newuser@example.com")
|
|
|
143
166
|
# Change user password
|
|
144
167
|
client.users.passwd("newuser", "NewPassword123", "TempPassword123")
|
|
145
168
|
|
|
169
|
+
# Reset the password of another user (admin), without the current password
|
|
170
|
+
client.users.passwd("newuser", "NewPassword123")
|
|
171
|
+
|
|
146
172
|
# Delete a user
|
|
147
173
|
client.users.delete("newuser")
|
|
148
174
|
```
|
|
@@ -181,6 +207,43 @@ client.groups.remove_member_manager("developers", user: ["manager"])
|
|
|
181
207
|
client.groups.delete("developers")
|
|
182
208
|
```
|
|
183
209
|
|
|
210
|
+
##### Vaults
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
client = Fripa::Client.new(username: 'admin', password: 'secret')
|
|
214
|
+
|
|
215
|
+
client.vault.add("my-vault", shared: true)
|
|
216
|
+
client.vault.archive("my-vault", data: "secret", shared: true)
|
|
217
|
+
client.vault.retrieve("my-vault", shared: true) # => "secret"
|
|
218
|
+
client.vault.add_member("my-vault", user: ["alice"], shared: true)
|
|
219
|
+
client.vault.delete("my-vault", shared: true)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
By default, `archive` stores the raw bytes. The `ipa` CLI stores the data in a
|
|
223
|
+
JSON envelope (`{"data": "<base64>"}`). To share a vault with the `ipa` CLI
|
|
224
|
+
(`ipa vault-archive` / `ipa vault-retrieve`), pass `ipa_format: true`:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
client.vault.archive("my-vault", data: "secret", ipa_format: true, shared: true)
|
|
228
|
+
client.vault.retrieve("my-vault", ipa_format: true, shared: true) # => "secret"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The client fetches the KRA transport certificate once and keeps it.
|
|
232
|
+
|
|
233
|
+
#### Batch Calls
|
|
234
|
+
|
|
235
|
+
Send many commands in one HTTP request:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
result = client.batch(
|
|
239
|
+
["user_show", ["alice"]],
|
|
240
|
+
["group_find", [], { sizelimit: 0 }]
|
|
241
|
+
)
|
|
242
|
+
result.dig("result", "results").each { |r| puts r["error"] || r["result"] }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`batch` does not raise for errors in a single command. Check `error` in each result.
|
|
246
|
+
|
|
184
247
|
#### Using Raw API Calls
|
|
185
248
|
|
|
186
249
|
For API methods without a resource wrapper, use the `call` method directly:
|
|
@@ -193,7 +256,31 @@ result = client.call("user_find", ["admin"], { all: true })
|
|
|
193
256
|
puts result.dig("result", "count")
|
|
194
257
|
```
|
|
195
258
|
|
|
196
|
-
|
|
259
|
+
If the session expires (HTTP 401), the client logs in again and retries the call once.
|
|
260
|
+
|
|
261
|
+
### Error Handling
|
|
262
|
+
|
|
263
|
+
All errors inherit from `Fripa::Error`:
|
|
264
|
+
|
|
265
|
+
| Error | When |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `Fripa::AuthenticationError` | Login fails |
|
|
268
|
+
| `Fripa::ConnectionError` | Network error, timeout, HTTP error status, or a response that is not JSON |
|
|
269
|
+
| `Fripa::ResponseError` | FreeIPA returns an error (`#code`, `#message`, `#data`) |
|
|
270
|
+
| `Fripa::PartialFailureError` | Only with `config.raise_on_partial_failure = true`. A subclass of `ResponseError`. The call succeeds but some entries fail, e.g. `group_add_member` with an unknown user. `#data` has the full `failed` hash |
|
|
271
|
+
|
|
272
|
+
By default, these per-entry failures do not raise. They stay in the response, in
|
|
273
|
+
`result["result"]["failed"]`. This will change in 1.0.
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
begin
|
|
277
|
+
client.groups.add_member("developers", user: ["alice"])
|
|
278
|
+
rescue Fripa::PartialFailureError => e
|
|
279
|
+
e.data # => {"member" => {"user" => [["alice", "This entry is already a member"]], "group" => []}}
|
|
280
|
+
rescue Fripa::ResponseError => e
|
|
281
|
+
puts "#{e.code}: #{e.message}"
|
|
282
|
+
end
|
|
283
|
+
```
|
|
197
284
|
|
|
198
285
|
## Development
|
|
199
286
|
|
data/lib/fripa/authenticator.rb
CHANGED
|
@@ -7,6 +7,9 @@ module Fripa
|
|
|
7
7
|
class Authenticator
|
|
8
8
|
LOGIN_PATH = "/ipa/session/login_password"
|
|
9
9
|
CONTENT_TYPE = "application/x-www-form-urlencoded"
|
|
10
|
+
# Splits a joined Set-Cookie header ("a=1; path=/, b=2") between cookies,
|
|
11
|
+
# but not inside attributes such as "Expires=Wed, 21 Oct 2026".
|
|
12
|
+
COOKIE_SEPARATOR = /,\s*(?=[^\s;,=]+=)/
|
|
10
13
|
|
|
11
14
|
attr_reader :client
|
|
12
15
|
|
|
@@ -20,7 +23,11 @@ module Fripa
|
|
|
20
23
|
|
|
21
24
|
raise AuthenticationError, "Login failed: #{response.status}" unless response.success?
|
|
22
25
|
|
|
23
|
-
response.headers["set-cookie"]
|
|
26
|
+
session_cookie(response.headers["set-cookie"])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inspect
|
|
30
|
+
"#<#{self.class.name} username=#{client.username.inspect}>"
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
private
|
|
@@ -31,19 +38,26 @@ module Fripa
|
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def perform_login
|
|
34
|
-
connection.post(LOGIN_PATH) do |request|
|
|
41
|
+
client.connection.post(LOGIN_PATH) do |request|
|
|
35
42
|
request.headers["Content-Type"] = CONTENT_TYPE
|
|
36
|
-
request.headers["Referer"] =
|
|
37
|
-
request.body =
|
|
43
|
+
request.headers["Referer"] = client.referer
|
|
44
|
+
request.body = login_body
|
|
38
45
|
end
|
|
46
|
+
rescue Faraday::Error => e
|
|
47
|
+
raise ConnectionError, "Login failed: #{e.message}"
|
|
39
48
|
end
|
|
40
49
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
def login_body
|
|
51
|
+
URI.encode_www_form(user: client.username, password: client.password)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Keeps only "name=value" pairs: attributes like path, httponly or secure
|
|
55
|
+
# must not be sent back in the Cookie header. Returns nil without a
|
|
56
|
+
# Set-Cookie header, as before (recorded test cassettes often drop it).
|
|
57
|
+
def session_cookie(header)
|
|
58
|
+
return nil if header.nil?
|
|
59
|
+
|
|
60
|
+
header.split(COOKIE_SEPARATOR).map { |cookie| cookie.split(";").first.to_s.strip }.reject(&:empty?).join("; ")
|
|
47
61
|
end
|
|
48
62
|
end
|
|
49
63
|
end
|
data/lib/fripa/client.rb
CHANGED
|
@@ -6,7 +6,8 @@ require "json"
|
|
|
6
6
|
module Fripa
|
|
7
7
|
class Client
|
|
8
8
|
API_PATH = "/ipa/session/json"
|
|
9
|
-
API_VERSION =
|
|
9
|
+
API_VERSION = Configuration::DEFAULT_API_VERSION
|
|
10
|
+
UNAUTHORIZED = 401
|
|
10
11
|
|
|
11
12
|
attr_reader :config, :username, :password
|
|
12
13
|
attr_accessor :session_cookie
|
|
@@ -36,54 +37,88 @@ module Fripa
|
|
|
36
37
|
|
|
37
38
|
def call(method, args = [], options = {})
|
|
38
39
|
response = perform_request(method, args, options)
|
|
40
|
+
response = reauthenticate_and_retry(method, args, options) if response.status == UNAUTHORIZED
|
|
39
41
|
|
|
40
42
|
raise ConnectionError, "API call failed: #{response.status}" unless response.success?
|
|
41
43
|
|
|
42
44
|
parse_response(response)
|
|
43
45
|
end
|
|
44
46
|
|
|
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
|
+
# Shared by Authenticator, so login and API calls reuse one connection.
|
|
58
|
+
def connection
|
|
59
|
+
@connection ||= Faraday.new(url: config.base_url.to_s, request: request_options) do |f|
|
|
60
|
+
f.adapter(*Array(config.adapter || Faraday.default_adapter))
|
|
61
|
+
f.ssl.verify = config.verify_ssl
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def referer
|
|
66
|
+
@referer ||= "#{config.base_url}/ipa"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def inspect
|
|
70
|
+
"#<#{self.class.name} username=#{username.inspect} host=#{config.host.inspect}>"
|
|
71
|
+
end
|
|
72
|
+
|
|
45
73
|
private
|
|
46
74
|
|
|
75
|
+
def reauthenticate_and_retry(method, args, options)
|
|
76
|
+
self.session_cookie = authenticator.login!
|
|
77
|
+
perform_request(method, args, options)
|
|
78
|
+
end
|
|
79
|
+
|
|
47
80
|
def perform_request(method, args, options)
|
|
48
81
|
connection.post(API_PATH) do |request|
|
|
49
82
|
request.headers["Content-Type"] = "application/json"
|
|
50
|
-
request.headers["Referer"] =
|
|
83
|
+
request.headers["Referer"] = referer
|
|
51
84
|
request.headers["Cookie"] = session_cookie
|
|
52
85
|
request.body = build_request_body(method, args, options).to_json
|
|
53
86
|
end
|
|
87
|
+
rescue Faraday::Error => e
|
|
88
|
+
raise ConnectionError, "API call failed: #{e.message}"
|
|
54
89
|
end
|
|
55
90
|
|
|
56
91
|
def build_request_body(method, args, options)
|
|
57
92
|
{
|
|
58
93
|
method: method,
|
|
59
|
-
params: [args, options.merge(version:
|
|
94
|
+
params: [args, options.merge(version: config.api_version)],
|
|
60
95
|
id: 0
|
|
61
96
|
}
|
|
62
97
|
end
|
|
63
98
|
|
|
64
99
|
def parse_response(response)
|
|
65
100
|
parsed = JSON.parse(response.body)
|
|
101
|
+
raise ConnectionError, "API call failed: unexpected response" unless parsed.is_a?(Hash)
|
|
102
|
+
|
|
66
103
|
raise_if_error(parsed)
|
|
67
104
|
parsed
|
|
105
|
+
rescue JSON::ParserError
|
|
106
|
+
raise ConnectionError, "API call failed: invalid JSON response"
|
|
68
107
|
end
|
|
69
108
|
|
|
70
109
|
def raise_if_error(parsed)
|
|
71
|
-
|
|
110
|
+
if (error = parsed["error"])
|
|
111
|
+
raise ResponseError.new(code: error["code"], message: error["message"] || error["name"], data: error["data"])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
return unless config.raise_on_partial_failure
|
|
72
115
|
|
|
73
|
-
|
|
74
|
-
raise
|
|
75
|
-
code: error["code"],
|
|
76
|
-
message: error["message"] || error["name"],
|
|
77
|
-
data: error["data"]
|
|
78
|
-
)
|
|
116
|
+
failed = parsed.dig("result", "failed")
|
|
117
|
+
raise PartialFailureError, failed if PartialFailureError.failures(failed).any?
|
|
79
118
|
end
|
|
80
119
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
f.request :url_encoded
|
|
84
|
-
f.adapter Faraday.default_adapter
|
|
85
|
-
f.ssl.verify = config.verify_ssl
|
|
86
|
-
end
|
|
120
|
+
def request_options
|
|
121
|
+
{ timeout: config.timeout, open_timeout: config.open_timeout }.compact
|
|
87
122
|
end
|
|
88
123
|
end
|
|
89
124
|
end
|
data/lib/fripa/configuration.rb
CHANGED
|
@@ -6,13 +6,23 @@ require "uri"
|
|
|
6
6
|
module Fripa
|
|
7
7
|
# Configuration for Fripa
|
|
8
8
|
class Configuration
|
|
9
|
-
|
|
9
|
+
DEFAULT_API_VERSION = "2.251"
|
|
10
|
+
SCHEMES = %w[http https].freeze
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
attr_accessor :host, :port, :scheme, :verify_ssl, :api_version, :timeout, :open_timeout, :adapter,
|
|
13
|
+
:raise_on_partial_failure
|
|
14
|
+
|
|
15
|
+
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)
|
|
12
17
|
@host = host
|
|
13
18
|
@port = port
|
|
14
19
|
@scheme = scheme
|
|
15
20
|
@verify_ssl = verify_ssl
|
|
21
|
+
@api_version = api_version
|
|
22
|
+
@timeout = timeout
|
|
23
|
+
@open_timeout = open_timeout
|
|
24
|
+
@adapter = adapter
|
|
25
|
+
@raise_on_partial_failure = raise_on_partial_failure
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
def base_url
|
|
@@ -30,9 +40,12 @@ module Fripa
|
|
|
30
40
|
private
|
|
31
41
|
|
|
32
42
|
def build_uri(path: nil)
|
|
33
|
-
raise ArgumentError, "host is required" if host.
|
|
43
|
+
raise ArgumentError, "host is required" if host.to_s.empty?
|
|
44
|
+
|
|
45
|
+
normalized_scheme = scheme.to_s.downcase
|
|
46
|
+
raise ArgumentError, "scheme must be one of: #{SCHEMES.join(", ")}" unless SCHEMES.include?(normalized_scheme)
|
|
34
47
|
|
|
35
|
-
uri_class =
|
|
48
|
+
uri_class = normalized_scheme == "https" ? URI::HTTPS : URI::HTTP
|
|
36
49
|
uri_class.build(host: host, port: port, path: path)
|
|
37
50
|
end
|
|
38
51
|
end
|
data/lib/fripa/errors.rb
CHANGED
|
@@ -14,4 +14,27 @@ module Fripa
|
|
|
14
14
|
super(message)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# Raised when FreeIPA answers without an error but reports per-entry failures,
|
|
19
|
+
# e.g. group_add_member with a user that does not exist or is already a member.
|
|
20
|
+
# Only raised with config.raise_on_partial_failure = true.
|
|
21
|
+
# #data holds the full "failed" hash from the response.
|
|
22
|
+
class PartialFailureError < ResponseError
|
|
23
|
+
def initialize(failed)
|
|
24
|
+
super(code: nil, message: self.class.first_reason(failed), data: failed)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.failures(failed)
|
|
28
|
+
return [] unless failed.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
failed.values.flat_map do |types|
|
|
31
|
+
types.is_a?(Hash) ? types.values.flat_map { |entries| Array(entries) } : Array(types)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.first_reason(failed)
|
|
36
|
+
entry = failures(failed).first
|
|
37
|
+
entry.is_a?(Array) ? entry.last.to_s : entry.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
17
40
|
end
|
data/lib/fripa/resources/user.rb
CHANGED
|
@@ -32,11 +32,13 @@ module Fripa
|
|
|
32
32
|
call("user_del", [uid])
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# current_password is only needed when users change their own password.
|
|
36
|
+
# Admins can omit it to reset the password of another user.
|
|
37
|
+
def passwd(uid, new_password, current_password = nil, **options)
|
|
36
38
|
validate_identifier!(uid, "uid")
|
|
37
39
|
validate_password!(new_password, "new_password")
|
|
38
|
-
validate_password!(current_password, "current_password")
|
|
39
|
-
call("passwd", [uid, new_password, current_password], options)
|
|
40
|
+
validate_password!(current_password, "current_password") unless current_password.nil?
|
|
41
|
+
call("passwd", [uid, new_password, current_password].compact, options)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
private
|
|
@@ -15,24 +15,30 @@ module Fripa
|
|
|
15
15
|
call("vault_add_internal", [cn], { ipavaulttype: "standard" }.merge(scope))
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# ipa_format: true stores the data like `ipa vault-archive`, so the ipa CLI can read it.
|
|
19
|
+
# The default (false) stores the raw bytes, as in previous versions.
|
|
20
|
+
def archive(cn, data:, ipa_format: false, **scope)
|
|
19
21
|
validate_identifier!(cn, "cn")
|
|
20
|
-
|
|
22
|
+
data = VaultTransport.to_ipa_format(data) if ipa_format
|
|
21
23
|
payload = VaultTransport.wrap(data, transport_cert_der)
|
|
22
|
-
call("vault_archive_internal", [cn],
|
|
24
|
+
call("vault_archive_internal", [cn],
|
|
25
|
+
binary_encode(payload).merge(wrapping_algo: VaultTransport::WRAPPING_ALGO).merge(scope))
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
# ipa_format: true reads data stored by `ipa vault-archive` (or archive with ipa_format: true).
|
|
29
|
+
# Returns nil if the vault has no data.
|
|
30
|
+
def retrieve(cn, ipa_format: false, **scope)
|
|
26
31
|
validate_identifier!(cn, "cn")
|
|
27
32
|
session_key = VaultTransport.generate_session_key
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
result = call_retrieve(cn, session_key, **scope).dig("result", "result")
|
|
34
|
+
return nil unless result&.key?("vault_data")
|
|
30
35
|
|
|
31
|
-
VaultTransport.decrypt(
|
|
36
|
+
data = VaultTransport.decrypt(
|
|
32
37
|
vault_data: extract_value(result["vault_data"]),
|
|
33
38
|
nonce: extract_value(result["nonce"]),
|
|
34
39
|
session_key: session_key
|
|
35
40
|
)
|
|
41
|
+
ipa_format ? VaultTransport.from_ipa_format(data) : data
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def add_member(cn, **scope)
|
|
@@ -47,15 +53,19 @@ module Fripa
|
|
|
47
53
|
|
|
48
54
|
private
|
|
49
55
|
|
|
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
|
+
def transport_cert_der
|
|
59
|
+
@transport_cert_der ||= fetch_transport_cert
|
|
60
|
+
end
|
|
61
|
+
|
|
50
62
|
def fetch_transport_cert
|
|
51
63
|
response = call("vaultconfig_show", [], { all: true })
|
|
52
|
-
|
|
53
|
-
Base64.decode64(cert.is_a?(Hash) ? cert["__base64__"] : cert)
|
|
64
|
+
Base64.decode64(extract_value(response.dig("result", "result", "transport_cert")))
|
|
54
65
|
end
|
|
55
66
|
|
|
56
67
|
def call_retrieve(cn, session_key, **scope)
|
|
57
|
-
|
|
58
|
-
encrypted_key = cert.public_key.public_encrypt(session_key, VaultTransport::RSA_PADDING)
|
|
68
|
+
encrypted_key = VaultTransport.encrypt_session_key(session_key, transport_cert_der)
|
|
59
69
|
|
|
60
70
|
call("vault_retrieve_internal", [cn], {
|
|
61
71
|
session_key: { "__base64__" => Base64.strict_encode64(encrypted_key) },
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "base64"
|
|
5
|
+
require "json"
|
|
5
6
|
|
|
6
7
|
module Fripa
|
|
7
8
|
module VaultTransport
|
|
@@ -51,12 +52,17 @@ module Fripa
|
|
|
51
52
|
|
|
52
53
|
def self.unwrap(vault_data:, session_key:, nonce:, private_key:)
|
|
53
54
|
decrypted_session_key = private_key.private_decrypt(Base64.decode64(session_key), RSA_PADDING)
|
|
55
|
+
decrypt(vault_data: vault_data, nonce: nonce, session_key: decrypted_session_key)
|
|
56
|
+
end
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
# The ipa CLI (ipa vault-archive / vault-retrieve) stores {"data": base64(data)},
|
|
59
|
+
# not the raw bytes. Use these to read or write vaults shared with the CLI.
|
|
60
|
+
def self.to_ipa_format(data)
|
|
61
|
+
JSON.generate("data" => Base64.strict_encode64(data))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.from_ipa_format(json)
|
|
65
|
+
Base64.strict_decode64(JSON.parse(json).fetch("data"))
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
def self.generate_transport_key
|
data/lib/fripa/version.rb
CHANGED
data/sig/fripa.rbs
CHANGED
|
@@ -1,4 +1,148 @@
|
|
|
1
1
|
module Fripa
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
type json_hash = Hash[String, untyped]
|
|
5
|
+
|
|
6
|
+
def self.config: () -> Configuration
|
|
7
|
+
def self.config=: (Configuration | Hash[Symbol, untyped] value) -> Configuration
|
|
8
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
9
|
+
|
|
10
|
+
class Configuration
|
|
11
|
+
DEFAULT_API_VERSION: String
|
|
12
|
+
SCHEMES: Array[String]
|
|
13
|
+
|
|
14
|
+
attr_accessor host: String?
|
|
15
|
+
attr_accessor port: Integer?
|
|
16
|
+
attr_accessor scheme: String
|
|
17
|
+
attr_accessor verify_ssl: bool
|
|
18
|
+
attr_accessor api_version: String
|
|
19
|
+
attr_accessor timeout: Numeric?
|
|
20
|
+
attr_accessor open_timeout: Numeric?
|
|
21
|
+
attr_accessor adapter: untyped
|
|
22
|
+
attr_accessor raise_on_partial_failure: bool
|
|
23
|
+
|
|
24
|
+
def initialize: (?host: String?, ?port: Integer?, ?scheme: String, ?verify_ssl: bool, ?api_version: String,
|
|
25
|
+
?timeout: Numeric?, ?open_timeout: Numeric?, ?adapter: untyped,
|
|
26
|
+
?raise_on_partial_failure: bool) -> void
|
|
27
|
+
def base_url: () -> URI::Generic
|
|
28
|
+
def login_url: () -> URI::Generic
|
|
29
|
+
def api_url: () -> URI::Generic
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def build_uri: (?path: String?) -> URI::Generic
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Error < StandardError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class AuthenticationError < Error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ConnectionError < Error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class ResponseError < Error
|
|
46
|
+
attr_reader code: Integer?
|
|
47
|
+
attr_reader data: untyped
|
|
48
|
+
|
|
49
|
+
def initialize: (code: Integer?, message: String?, ?data: untyped) -> void
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class PartialFailureError < ResponseError
|
|
53
|
+
def initialize: (json_hash failed) -> void
|
|
54
|
+
def self.failures: (untyped failed) -> Array[untyped]
|
|
55
|
+
def self.first_reason: (untyped failed) -> String
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Client
|
|
59
|
+
API_PATH: String
|
|
60
|
+
API_VERSION: String
|
|
61
|
+
UNAUTHORIZED: Integer
|
|
62
|
+
|
|
63
|
+
attr_reader config: Configuration
|
|
64
|
+
attr_reader username: String
|
|
65
|
+
attr_reader password: String
|
|
66
|
+
attr_accessor session_cookie: String?
|
|
67
|
+
|
|
68
|
+
def initialize: (username: String, password: String, ?config: Configuration?) -> void
|
|
69
|
+
def users: () -> Resources::User
|
|
70
|
+
def groups: () -> Resources::Group
|
|
71
|
+
def vault: () -> Resources::Vault
|
|
72
|
+
def authenticator: () -> Authenticator
|
|
73
|
+
def call: (String method, ?Array[untyped] args, ?Hash[Symbol, untyped] options) -> json_hash
|
|
74
|
+
def batch: (*Array[untyped] commands) -> json_hash
|
|
75
|
+
def connection: () -> untyped
|
|
76
|
+
def referer: () -> String
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class Authenticator
|
|
80
|
+
LOGIN_PATH: String
|
|
81
|
+
CONTENT_TYPE: String
|
|
82
|
+
COOKIE_SEPARATOR: Regexp
|
|
83
|
+
|
|
84
|
+
attr_reader client: Client
|
|
85
|
+
|
|
86
|
+
def initialize: (Client client) -> void
|
|
87
|
+
def login!: () -> String?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
module VaultTransport
|
|
91
|
+
CIPHER: String
|
|
92
|
+
WRAPPING_ALGO: String
|
|
93
|
+
KEY_SIZE: Integer
|
|
94
|
+
IV_SIZE: Integer
|
|
95
|
+
RSA_KEY_SIZE: Integer
|
|
96
|
+
RSA_PADDING: Integer
|
|
97
|
+
|
|
98
|
+
def self.generate_session_key: () -> String
|
|
99
|
+
def self.decrypt: (vault_data: String, nonce: String, session_key: String) -> String
|
|
100
|
+
def self.wrap: (String data, String transport_cert_der) -> { vault_data: String, session_key: String, nonce: String }
|
|
101
|
+
def self.encrypt_data: (String data, String key, String nonce) -> String
|
|
102
|
+
def self.encrypt_session_key: (String session_key, String cert_der) -> String
|
|
103
|
+
def self.unwrap: (vault_data: String, session_key: String, nonce: String, private_key: OpenSSL::PKey::RSA) -> String
|
|
104
|
+
def self.to_ipa_format: (String data) -> String
|
|
105
|
+
def self.from_ipa_format: (String json) -> String
|
|
106
|
+
def self.generate_transport_key: () -> OpenSSL::PKey::RSA
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
module Resources
|
|
110
|
+
class Base
|
|
111
|
+
attr_reader client: Client
|
|
112
|
+
|
|
113
|
+
def initialize: (Client client) -> void
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class User < Base
|
|
117
|
+
REQUIRED_ADD_FIELDS: Array[Symbol]
|
|
118
|
+
|
|
119
|
+
def find: (?String? uid, **untyped options) -> json_hash
|
|
120
|
+
def show: (String uid) -> json_hash
|
|
121
|
+
def add: (String uid, **untyped attributes) -> json_hash
|
|
122
|
+
def mod: (String uid, **untyped attributes) -> json_hash
|
|
123
|
+
def delete: (String uid) -> json_hash
|
|
124
|
+
def passwd: (String uid, String new_password, ?String? current_password, **untyped options) -> json_hash
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Group < Base
|
|
128
|
+
def find: (?String? cn, **untyped options) -> json_hash
|
|
129
|
+
def show: (String cn) -> json_hash
|
|
130
|
+
def add: (String cn, **untyped attributes) -> json_hash
|
|
131
|
+
def mod: (String cn, **untyped attributes) -> json_hash
|
|
132
|
+
def delete: (String cn) -> json_hash
|
|
133
|
+
def add_member: (String cn, **untyped members) -> json_hash
|
|
134
|
+
def remove_member: (String cn, **untyped members) -> json_hash
|
|
135
|
+
def add_member_manager: (String cn, **untyped managers) -> json_hash
|
|
136
|
+
def remove_member_manager: (String cn, **untyped managers) -> json_hash
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class Vault < Base
|
|
140
|
+
def show: (String cn, **untyped scope) -> json_hash
|
|
141
|
+
def add: (String cn, **untyped scope) -> json_hash
|
|
142
|
+
def archive: (String cn, data: String, ?ipa_format: bool, **untyped scope) -> json_hash
|
|
143
|
+
def retrieve: (String cn, ?ipa_format: bool, **untyped scope) -> String?
|
|
144
|
+
def add_member: (String cn, **untyped scope) -> json_hash
|
|
145
|
+
def delete: (String cn, **untyped scope) -> json_hash
|
|
146
|
+
end
|
|
147
|
+
end
|
|
4
148
|
end
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fripa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Costanzo
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: base64
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
18
|
+
version: '0.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
25
|
+
version: '0.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: faraday
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,21 +37,17 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.0'
|
|
40
|
-
description: A Ruby client library for
|
|
40
|
+
description: 'A Ruby client library for FreeIPA''s JSON-RPC API: users, groups, vaults
|
|
41
|
+
and batch calls.'
|
|
41
42
|
email:
|
|
42
43
|
- dev.bcostanzo@gmail.com
|
|
43
44
|
executables: []
|
|
44
45
|
extensions: []
|
|
45
46
|
extra_rdoc_files: []
|
|
46
47
|
files:
|
|
47
|
-
- ".env.example"
|
|
48
|
-
- ".simplecov"
|
|
49
48
|
- CHANGELOG.md
|
|
50
|
-
- CODE_OF_CONDUCT.md
|
|
51
|
-
- CONTRIBUTING.md
|
|
52
49
|
- LICENSE.txt
|
|
53
50
|
- README.md
|
|
54
|
-
- Rakefile
|
|
55
51
|
- lib/fripa.rb
|
|
56
52
|
- lib/fripa/authenticator.rb
|
|
57
53
|
- lib/fripa/client.rb
|
|
@@ -68,9 +64,11 @@ homepage: https://github.com/renuo/fripa
|
|
|
68
64
|
licenses:
|
|
69
65
|
- MIT
|
|
70
66
|
metadata:
|
|
71
|
-
homepage_uri: https://github.com/renuo/fripa
|
|
72
67
|
source_code_uri: https://github.com/renuo/fripa
|
|
73
68
|
changelog_uri: https://github.com/renuo/fripa/blob/main/CHANGELOG.md
|
|
69
|
+
bug_tracker_uri: https://github.com/renuo/fripa/issues
|
|
70
|
+
documentation_uri: https://rubydoc.info/gems/fripa
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
74
72
|
rdoc_options: []
|
|
75
73
|
require_paths:
|
|
76
74
|
- lib
|
|
@@ -85,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
85
83
|
- !ruby/object:Gem::Version
|
|
86
84
|
version: '0'
|
|
87
85
|
requirements: []
|
|
88
|
-
rubygems_version: 4.0.
|
|
86
|
+
rubygems_version: 4.0.10
|
|
89
87
|
specification_version: 4
|
|
90
88
|
summary: Ruby client for FreeIPA JSON-RPC API
|
|
91
89
|
test_files: []
|
data/.env.example
DELETED
data/.simplecov
DELETED
data/CODE_OF_CONDUCT.md
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# Contributor Covenant Code of Conduct
|
|
2
|
-
|
|
3
|
-
## Our Pledge
|
|
4
|
-
|
|
5
|
-
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
-
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
-
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
-
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
-
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
10
|
-
identity and orientation.
|
|
11
|
-
|
|
12
|
-
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
-
diverse, inclusive, and healthy community.
|
|
14
|
-
|
|
15
|
-
## Our Standards
|
|
16
|
-
|
|
17
|
-
Examples of behavior that contributes to a positive environment for our
|
|
18
|
-
community include:
|
|
19
|
-
|
|
20
|
-
* Demonstrating empathy and kindness toward other people
|
|
21
|
-
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
-
* Giving and gracefully accepting constructive feedback
|
|
23
|
-
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
-
and learning from the experience
|
|
25
|
-
* Focusing on what is best not just for us as individuals, but for the overall
|
|
26
|
-
community
|
|
27
|
-
|
|
28
|
-
Examples of unacceptable behavior include:
|
|
29
|
-
|
|
30
|
-
* The use of sexualized language or imagery, and sexual attention or advances of
|
|
31
|
-
any kind
|
|
32
|
-
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
-
* Public or private harassment
|
|
34
|
-
* Publishing others' private information, such as a physical or email address,
|
|
35
|
-
without their explicit permission
|
|
36
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
-
professional setting
|
|
38
|
-
|
|
39
|
-
## Enforcement Responsibilities
|
|
40
|
-
|
|
41
|
-
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
-
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
-
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
-
or harmful.
|
|
45
|
-
|
|
46
|
-
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
-
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
-
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
-
decisions when appropriate.
|
|
50
|
-
|
|
51
|
-
## Scope
|
|
52
|
-
|
|
53
|
-
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
-
an individual is officially representing the community in public spaces.
|
|
55
|
-
Examples of representing our community include using an official email address,
|
|
56
|
-
posting via an official social media account, or acting as an appointed
|
|
57
|
-
representative at an online or offline event.
|
|
58
|
-
|
|
59
|
-
## Enforcement
|
|
60
|
-
|
|
61
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
-
reported to the community leaders responsible for enforcement at
|
|
63
|
-
[INSERT CONTACT METHOD].
|
|
64
|
-
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
-
|
|
66
|
-
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
-
reporter of any incident.
|
|
68
|
-
|
|
69
|
-
## Enforcement Guidelines
|
|
70
|
-
|
|
71
|
-
Community leaders will follow these Community Impact Guidelines in determining
|
|
72
|
-
the consequences for any action they deem in violation of this Code of Conduct:
|
|
73
|
-
|
|
74
|
-
### 1. Correction
|
|
75
|
-
|
|
76
|
-
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
77
|
-
unprofessional or unwelcome in the community.
|
|
78
|
-
|
|
79
|
-
**Consequence**: A private, written warning from community leaders, providing
|
|
80
|
-
clarity around the nature of the violation and an explanation of why the
|
|
81
|
-
behavior was inappropriate. A public apology may be requested.
|
|
82
|
-
|
|
83
|
-
### 2. Warning
|
|
84
|
-
|
|
85
|
-
**Community Impact**: A violation through a single incident or series of
|
|
86
|
-
actions.
|
|
87
|
-
|
|
88
|
-
**Consequence**: A warning with consequences for continued behavior. No
|
|
89
|
-
interaction with the people involved, including unsolicited interaction with
|
|
90
|
-
those enforcing the Code of Conduct, for a specified period of time. This
|
|
91
|
-
includes avoiding interactions in community spaces as well as external channels
|
|
92
|
-
like social media. Violating these terms may lead to a temporary or permanent
|
|
93
|
-
ban.
|
|
94
|
-
|
|
95
|
-
### 3. Temporary Ban
|
|
96
|
-
|
|
97
|
-
**Community Impact**: A serious violation of community standards, including
|
|
98
|
-
sustained inappropriate behavior.
|
|
99
|
-
|
|
100
|
-
**Consequence**: A temporary ban from any sort of interaction or public
|
|
101
|
-
communication with the community for a specified period of time. No public or
|
|
102
|
-
private interaction with the people involved, including unsolicited interaction
|
|
103
|
-
with those enforcing the Code of Conduct, is allowed during this period.
|
|
104
|
-
Violating these terms may lead to a permanent ban.
|
|
105
|
-
|
|
106
|
-
### 4. Permanent Ban
|
|
107
|
-
|
|
108
|
-
**Community Impact**: Demonstrating a pattern of violation of community
|
|
109
|
-
standards, including sustained inappropriate behavior, harassment of an
|
|
110
|
-
individual, or aggression toward or disparagement of classes of individuals.
|
|
111
|
-
|
|
112
|
-
**Consequence**: A permanent ban from any sort of public interaction within the
|
|
113
|
-
community.
|
|
114
|
-
|
|
115
|
-
## Attribution
|
|
116
|
-
|
|
117
|
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
118
|
-
version 2.1, available at
|
|
119
|
-
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
120
|
-
|
|
121
|
-
Community Impact Guidelines were inspired by
|
|
122
|
-
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
123
|
-
|
|
124
|
-
For answers to common questions about this code of conduct, see the FAQ at
|
|
125
|
-
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
|
126
|
-
[https://www.contributor-covenant.org/translations][translations].
|
|
127
|
-
|
|
128
|
-
[homepage]: https://www.contributor-covenant.org
|
|
129
|
-
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
130
|
-
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
131
|
-
[FAQ]: https://www.contributor-covenant.org/faq
|
|
132
|
-
[translations]: https://www.contributor-covenant.org/translations
|
data/CONTRIBUTING.md
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# Contributing to Fripa
|
|
2
|
-
|
|
3
|
-
Thank you for your interest in contributing to Fripa! This document provides guidelines and instructions for contributing.
|
|
4
|
-
|
|
5
|
-
## How to Contribute
|
|
6
|
-
|
|
7
|
-
### Reporting Bugs
|
|
8
|
-
|
|
9
|
-
If you find a bug, please open an issue on GitHub with:
|
|
10
|
-
- A clear, descriptive title
|
|
11
|
-
- Steps to reproduce the issue
|
|
12
|
-
- Expected behavior vs. actual behavior
|
|
13
|
-
- Your Ruby version and FreeIPA version
|
|
14
|
-
- Any relevant code samples or error messages
|
|
15
|
-
|
|
16
|
-
### Suggesting Features
|
|
17
|
-
|
|
18
|
-
Feature suggestions are welcome! Please open an issue with:
|
|
19
|
-
- A clear description of the feature
|
|
20
|
-
- Use cases explaining why this feature would be useful
|
|
21
|
-
- Any implementation ideas you might have
|
|
22
|
-
|
|
23
|
-
### Pull Requests
|
|
24
|
-
|
|
25
|
-
1. **Fork the repository** and create your branch from `main`
|
|
26
|
-
2. **Make your changes** following our coding standards
|
|
27
|
-
3. **Add tests** for any new functionality
|
|
28
|
-
4. **Ensure all tests pass** by running `bin/test`
|
|
29
|
-
5. **Run the linter** with `bundle exec rubocop`
|
|
30
|
-
6. **Update documentation** if you're changing functionality
|
|
31
|
-
7. **Submit your pull request** with a clear description of changes
|
|
32
|
-
|
|
33
|
-
## Development Setup
|
|
34
|
-
|
|
35
|
-
1. Clone the repository:
|
|
36
|
-
```bash
|
|
37
|
-
git clone https://github.com/brunocostanzo/fripa.git
|
|
38
|
-
cd fripa
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
2. Install dependencies:
|
|
42
|
-
```bash
|
|
43
|
-
bin/setup
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
3. Run tests:
|
|
47
|
-
```bash
|
|
48
|
-
bin/test
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
4. Run linter:
|
|
52
|
-
```bash
|
|
53
|
-
bundle exec rubocop
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Coding Standards
|
|
57
|
-
|
|
58
|
-
- Follow the [Ruby Style Guide](https://rubystyle.guide/)
|
|
59
|
-
- Use RuboCop to check your code style
|
|
60
|
-
- Write clear, self-documenting code
|
|
61
|
-
- Add comments for complex logic
|
|
62
|
-
- Keep methods small and focused
|
|
63
|
-
- Maintain test coverage above 95%
|
|
64
|
-
|
|
65
|
-
## Testing Guidelines
|
|
66
|
-
|
|
67
|
-
- Write tests for all new features
|
|
68
|
-
- Update tests when modifying existing features
|
|
69
|
-
- Use VCR cassettes for API interactions
|
|
70
|
-
- Ensure both line and branch coverage remain high
|
|
71
|
-
- Test both success and error cases
|
|
72
|
-
|
|
73
|
-
## Commit Messages
|
|
74
|
-
|
|
75
|
-
Write clear, descriptive commit messages:
|
|
76
|
-
- Use present tense ("Add feature" not "Added feature")
|
|
77
|
-
- Capitalize the first letter
|
|
78
|
-
- Keep the first line under 72 characters
|
|
79
|
-
- Add detailed description if needed
|
|
80
|
-
|
|
81
|
-
Example:
|
|
82
|
-
```
|
|
83
|
-
Add support for host management
|
|
84
|
-
|
|
85
|
-
- Implement host resource with CRUD operations
|
|
86
|
-
- Add tests with VCR cassettes
|
|
87
|
-
- Update README with usage examples
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Questions?
|
|
91
|
-
|
|
92
|
-
If you have questions about contributing, feel free to open an issue labeled "question".
|
|
93
|
-
|
|
94
|
-
## Code of Conduct
|
|
95
|
-
|
|
96
|
-
By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
|
data/Rakefile
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "dotenv"
|
|
4
|
-
Dotenv.load
|
|
5
|
-
|
|
6
|
-
require "bundler/gem_tasks"
|
|
7
|
-
require "minitest/test_task"
|
|
8
|
-
|
|
9
|
-
Minitest::TestTask.create do |t|
|
|
10
|
-
t.warning = false
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
require "rubocop/rake_task"
|
|
14
|
-
|
|
15
|
-
RuboCop::RakeTask.new
|
|
16
|
-
|
|
17
|
-
task default: %i[test rubocop]
|