isds 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +44 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +346 -0
- data/Rakefile +16 -0
- data/lib/isds/attachment.rb +66 -0
- data/lib/isds/authentication/access_interface.rb +20 -0
- data/lib/isds/authentication/base.rb +34 -0
- data/lib/isds/authentication/basic.rb +17 -0
- data/lib/isds/authentication/certificate.rb +58 -0
- data/lib/isds/client.rb +81 -0
- data/lib/isds/configuration.rb +154 -0
- data/lib/isds/connection.rb +113 -0
- data/lib/isds/databox.rb +121 -0
- data/lib/isds/digital_signature/verifier.rb +64 -0
- data/lib/isds/error.rb +59 -0
- data/lib/isds/large_messages/downloader.rb +61 -0
- data/lib/isds/large_messages/uploader.rb +65 -0
- data/lib/isds/message.rb +208 -0
- data/lib/isds/search.rb +111 -0
- data/lib/isds/status.rb +65 -0
- data/lib/isds/timestamp/verifier.rb +36 -0
- data/lib/isds/types.rb +19 -0
- data/lib/isds/user.rb +71 -0
- data/lib/isds/version.rb +5 -0
- data/lib/isds.rb +57 -0
- metadata +213 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a5f6c4b4643e4f08410bb292d56afe96a75db717055ec20579dafea9da1dbadd
|
|
4
|
+
data.tar.gz: 7658f9a652173800354c7b8b2f5ce5d305d5c2d850237514df18027eccb86fa2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8682b7e75cd0e581350ae9c000f43a323e914acb5224a8501f88f706f9d227c99eb51cdfcc958898c8368c9c940c269fd9a4ef0be85a206e5b64088daa20f152
|
|
7
|
+
data.tar.gz: 662f41fd534dca323546dbe80e3d20984eac614e160c8193cba1af8bf9b36a8cf92d6c11c26497507cdc48eb748378573f95a6c0cf0df55be8e98ef8d842836d
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
TargetRubyVersion: 3.1
|
|
6
|
+
NewCops: enable
|
|
7
|
+
SuggestExtensions: false
|
|
8
|
+
Exclude:
|
|
9
|
+
- "vendor/**/*"
|
|
10
|
+
- "pkg/**/*"
|
|
11
|
+
- "*.md"
|
|
12
|
+
|
|
13
|
+
Style/Documentation:
|
|
14
|
+
Enabled: false
|
|
15
|
+
|
|
16
|
+
Style/FrozenStringLiteralComment:
|
|
17
|
+
Enabled: true
|
|
18
|
+
|
|
19
|
+
Metrics/ClassLength:
|
|
20
|
+
Max: 150
|
|
21
|
+
|
|
22
|
+
Metrics/MethodLength:
|
|
23
|
+
Max: 25
|
|
24
|
+
|
|
25
|
+
Metrics/BlockLength:
|
|
26
|
+
Exclude:
|
|
27
|
+
- "spec/**/*"
|
|
28
|
+
- "*.gemspec"
|
|
29
|
+
|
|
30
|
+
Layout/LineLength:
|
|
31
|
+
Max: 120
|
|
32
|
+
|
|
33
|
+
RSpec/ExampleLength:
|
|
34
|
+
Max: 20
|
|
35
|
+
|
|
36
|
+
RSpec/MultipleExpectations:
|
|
37
|
+
Max: 5
|
|
38
|
+
|
|
39
|
+
RSpec/NestedGroups:
|
|
40
|
+
Max: 4
|
|
41
|
+
|
|
42
|
+
RSpec/DescribeClass:
|
|
43
|
+
Exclude:
|
|
44
|
+
- "spec/integration/**/*"
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.2
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Segfault Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# ISDS
|
|
2
|
+
|
|
3
|
+
Ruby client for the Czech Data Box system (ISDS -- Informacni system datovych schranek).
|
|
4
|
+
|
|
5
|
+
Provides an idiomatic Ruby interface to the [ISDS SOAP web services](https://www.datoveschranky.info/) for sending and receiving official data messages, searching databoxes, managing users, and handling large messages (VoDZ).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby >= 3.1
|
|
10
|
+
- Active ISDS account (production or [test environment on czebox.cz](https://www.czebox.cz/))
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add to your Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'isds'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then run `bundle install`.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require 'isds'
|
|
26
|
+
|
|
27
|
+
client = ISDS::Client.new(ISDS::Configuration.new(
|
|
28
|
+
environment: :test, # :test (czebox.cz) or :production (mojedatovaschranka.cz)
|
|
29
|
+
auth_method: :basic,
|
|
30
|
+
username: 'your_username',
|
|
31
|
+
password: 'your_password'
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
client.authenticate!
|
|
35
|
+
puts client.owner_info
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
### Global configuration
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
ISDS.configure do |config|
|
|
44
|
+
config.environment = :production
|
|
45
|
+
config.auth_method = :basic
|
|
46
|
+
config.username = ENV.fetch('ISDS_USERNAME')
|
|
47
|
+
config.password = ENV.fetch('ISDS_PASSWORD')
|
|
48
|
+
config.timeout = 30
|
|
49
|
+
config.request_timeout = 120
|
|
50
|
+
config.enable_request_logging = false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
client = ISDS::Client.new
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Per-client configuration
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
config = ISDS::Configuration.new(
|
|
60
|
+
environment: :test,
|
|
61
|
+
auth_method: :basic,
|
|
62
|
+
username: 'user',
|
|
63
|
+
password: 'pass'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
client = ISDS::Client.new(config)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Authentication methods
|
|
70
|
+
|
|
71
|
+
**Basic authentication** (username + password):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
ISDS::Configuration.new(auth_method: :basic, username: 'user', password: 'pass')
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Certificate authentication**:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
ISDS::Configuration.new(
|
|
81
|
+
auth_method: :certificate,
|
|
82
|
+
certificate_path: '/path/to/cert.pem',
|
|
83
|
+
private_key_path: '/path/to/key.pem',
|
|
84
|
+
key_password: 'optional_key_password'
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### SSL options
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
ISDS::Configuration.new(
|
|
92
|
+
ssl_verify_peer: true,
|
|
93
|
+
ssl_ca_file: '/path/to/ca-bundle.crt',
|
|
94
|
+
ssl_version: :TLSv1_2
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Custom endpoints
|
|
99
|
+
|
|
100
|
+
Override default endpoints if needed:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
ISDS::Configuration.new(
|
|
104
|
+
endpoints: {
|
|
105
|
+
ws_url: 'https://custom-proxy.example.com/soap',
|
|
106
|
+
ws2_url: 'https://custom-proxy.example.com/vdz_ws/'
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Usage
|
|
112
|
+
|
|
113
|
+
### Messages
|
|
114
|
+
|
|
115
|
+
**Send a message:**
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
attachment = ISDS::Attachment.new(
|
|
119
|
+
filename: 'document.pdf',
|
|
120
|
+
file_path: '/path/to/document.pdf'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
message_id = client.send_message(
|
|
124
|
+
'abc1234', # recipient databox ID
|
|
125
|
+
subject: 'Test message',
|
|
126
|
+
attachments: [attachment],
|
|
127
|
+
personal_delivery: true, # optional
|
|
128
|
+
to_hands: 'Jan Novak' # optional
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**List received messages:**
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
messages = client.receive_messages(limit: 50, unread_only: true)
|
|
136
|
+
|
|
137
|
+
messages.each do |msg|
|
|
138
|
+
puts "#{msg.id}: #{msg.subject} from #{msg.sender}"
|
|
139
|
+
puts " Delivered: #{msg.delivered?}, Read: #{msg.read?}"
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**List sent messages:**
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
messages = client.sent_messages(limit: 20)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Download a specific message with attachments:**
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
message = client.find_message('12345678')
|
|
153
|
+
message.download_all_attachments('/tmp/attachments')
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Mark messages as read:**
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
client.mark_messages_read(['12345678', '12345679'])
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Databox search
|
|
163
|
+
|
|
164
|
+
**Search by name:**
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
results = client.search_databoxes('Ministerstvo')
|
|
168
|
+
results.each do |databox|
|
|
169
|
+
puts "#{databox.id}: #{databox.name} (#{databox.type})"
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Find by databox ID:**
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
databox = client.find_databox('abc1234')
|
|
177
|
+
puts databox.name if databox
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Advanced search:**
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# Search by ICO
|
|
184
|
+
ISDS::Search.by_ico('12345678', connection: client.connection)
|
|
185
|
+
|
|
186
|
+
# Search by name with exact matching
|
|
187
|
+
ISDS::Search.by_name('Exact Name s.r.o.', connection: client.connection, exact: true)
|
|
188
|
+
|
|
189
|
+
# Search by type (FO, PFO, PO, OVM)
|
|
190
|
+
ISDS::Search.by_type(:ovm, connection: client.connection, query: 'Praha')
|
|
191
|
+
|
|
192
|
+
# Search OVM only
|
|
193
|
+
ISDS::Search.ovm_only(connection: client.connection, query: 'Ministerstvo')
|
|
194
|
+
|
|
195
|
+
# Search by address
|
|
196
|
+
ISDS::Search.by_address(connection: client.connection, city: 'Praha', zip_code: '11000')
|
|
197
|
+
|
|
198
|
+
# Paginated search
|
|
199
|
+
page = ISDS::Search.paginated_search('Test', connection: client.connection, page: 1, per_page: 50)
|
|
200
|
+
page[:results].each { |db| puts db.name }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Check databox status:**
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
status = client.check_databox_status('abc1234')
|
|
207
|
+
puts status[:active] # => true/false
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Databox info
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
info = client.owner_info
|
|
214
|
+
password_info = client.password_info
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### User management
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# List users
|
|
221
|
+
users = ISDS::User.list(connection: client.connection)
|
|
222
|
+
|
|
223
|
+
# Add user
|
|
224
|
+
ISDS::User.add(
|
|
225
|
+
connection: client.connection,
|
|
226
|
+
databox_id: 'abc1234',
|
|
227
|
+
first_name: 'Jan',
|
|
228
|
+
last_name: 'Novak'
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Remove user
|
|
232
|
+
ISDS::User.remove(connection: client.connection, user_id: '123')
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Attachments
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
# From file
|
|
239
|
+
attachment = ISDS::Attachment.new(filename: 'doc.pdf', file_path: '/path/to/doc.pdf')
|
|
240
|
+
|
|
241
|
+
# From content
|
|
242
|
+
attachment = ISDS::Attachment.new(filename: 'doc.pdf', content: pdf_bytes)
|
|
243
|
+
|
|
244
|
+
# Properties
|
|
245
|
+
attachment.size # => byte size
|
|
246
|
+
attachment.mime_type # => 'application/pdf' (auto-detected)
|
|
247
|
+
attachment.encoded_content # => Base64-encoded string
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Standard message attachments are limited to 20 MB. Large messages (VoDZ) support up to 100 MB.
|
|
251
|
+
|
|
252
|
+
## Error handling
|
|
253
|
+
|
|
254
|
+
All ISDS errors inherit from `ISDS::Error`:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
begin
|
|
258
|
+
client.authenticate!
|
|
259
|
+
client.send_message('abc1234', subject: 'Test', attachments: [])
|
|
260
|
+
rescue ISDS::InvalidCredentialsError => e
|
|
261
|
+
puts "Auth failed: #{e.message}"
|
|
262
|
+
rescue ISDS::DataboxNotFoundError => e
|
|
263
|
+
puts "Databox not found: #{e.message}"
|
|
264
|
+
rescue ISDS::TimeoutError => e
|
|
265
|
+
puts "Request timed out: #{e.message}"
|
|
266
|
+
rescue ISDS::NetworkError => e
|
|
267
|
+
puts "Network issue: #{e.message}"
|
|
268
|
+
rescue ISDS::Error => e
|
|
269
|
+
puts "ISDS error [#{e.code}]: #{e.message}"
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Error hierarchy:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
ISDS::Error
|
|
277
|
+
ConfigurationError
|
|
278
|
+
NetworkError
|
|
279
|
+
TimeoutError
|
|
280
|
+
ConnectionError
|
|
281
|
+
AuthenticationError
|
|
282
|
+
InvalidCredentialsError
|
|
283
|
+
CertificateError
|
|
284
|
+
DataboxNotFoundError
|
|
285
|
+
MessageNotFoundError
|
|
286
|
+
PermissionDeniedError
|
|
287
|
+
InvalidMessageError
|
|
288
|
+
ServiceUnavailableError
|
|
289
|
+
TemporaryUnavailableError
|
|
290
|
+
QuotaExceededError
|
|
291
|
+
AttachmentError
|
|
292
|
+
FileSizeExceededError
|
|
293
|
+
UnsupportedFormatError
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
ISDS status codes are mapped automatically:
|
|
297
|
+
|
|
298
|
+
| Code | Error class |
|
|
299
|
+
|------|-------------|
|
|
300
|
+
| 0001 | `InvalidCredentialsError` |
|
|
301
|
+
| 0002 | `PermissionDeniedError` |
|
|
302
|
+
| 0003 | `DataboxNotFoundError` |
|
|
303
|
+
| 0004 | `MessageNotFoundError` |
|
|
304
|
+
| 0005 | `InvalidMessageError` |
|
|
305
|
+
| 0006 | `QuotaExceededError` |
|
|
306
|
+
| 9999 | `ServiceUnavailableError` |
|
|
307
|
+
|
|
308
|
+
## Endpoints
|
|
309
|
+
|
|
310
|
+
The gem uses the official ISDS SOAP endpoints:
|
|
311
|
+
|
|
312
|
+
| Environment | Standard operations | Large messages (VoDZ) |
|
|
313
|
+
|-------------|--------------------|-----------------------|
|
|
314
|
+
| Production | `https://ws1.mojedatovaschranka.cz/soap` | `https://ws1.mojedatovaschranka.cz/vdz_ws/` |
|
|
315
|
+
| Test | `https://www.czebox.cz/soap` | `https://www.czebox.cz/vdz_ws/` |
|
|
316
|
+
|
|
317
|
+
All standard services (operations, info, search, access, supplementary) are routed through the single `/soap` endpoint. Large message operations use the `/vdz_ws/` endpoint.
|
|
318
|
+
|
|
319
|
+
## Development
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Run unit tests
|
|
323
|
+
bundle exec rspec
|
|
324
|
+
|
|
325
|
+
# Run rubocop
|
|
326
|
+
bundle exec rubocop
|
|
327
|
+
|
|
328
|
+
# Run integration tests (requires czebox.cz credentials)
|
|
329
|
+
ISDS_USERNAME=your_user ISDS_PASSWORD=your_pass bundle exec rake integration
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Integration tests run against the [czebox.cz](https://www.czebox.cz/) test environment and are excluded from the default test suite.
|
|
333
|
+
|
|
334
|
+
## ISDS specification references
|
|
335
|
+
|
|
336
|
+
- [Provozni rad ISDS (PDF)](https://www.datoveschranky.info/documents/1744842/1746058/Provozni_rad_ISDS.pdf) -- official operational rules
|
|
337
|
+
- [ISDS web services documentation](https://www.datoveschranky.info/documents/1744842/1746058/WS_ISDS_Manipulace_s_datovymi_zpravami.pdf) -- SOAP API for message operations
|
|
338
|
+
- [ISDS WSDL definitions](https://www.czebox.cz/static/wsdl/v20/) -- WSDL schemas (test environment)
|
|
339
|
+
- [datoveschranky.info](https://www.datoveschranky.info/) -- official ISDS portal with full documentation
|
|
340
|
+
- [czebox.cz](https://www.czebox.cz/) -- ISDS test environment
|
|
341
|
+
|
|
342
|
+
The gem implements the ISDS v20 namespace (`http://isds.czechpoint.cz/v20`).
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
RSpec::Core::RakeTask.new(:integration) do |t|
|
|
9
|
+
t.rspec_opts = '--tag integration'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require 'rubocop/rake_task'
|
|
13
|
+
|
|
14
|
+
RuboCop::RakeTask.new
|
|
15
|
+
|
|
16
|
+
task default: %i[spec rubocop]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'mime/types'
|
|
5
|
+
|
|
6
|
+
module ISDS
|
|
7
|
+
class Attachment
|
|
8
|
+
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20MB per attachment (standard messages)
|
|
9
|
+
LARGE_MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB for VoDZ
|
|
10
|
+
|
|
11
|
+
attr_reader :filename, :mime_type, :content, :meta_type, :description
|
|
12
|
+
|
|
13
|
+
# rubocop:disable Metrics/ParameterLists -- all params are keyword args with sensible defaults
|
|
14
|
+
def initialize(filename:, content: nil, file_path: nil, mime_type: nil, meta_type: 'main', description: nil)
|
|
15
|
+
@filename = filename
|
|
16
|
+
@content = content || (file_path ? File.binread(file_path) : nil)
|
|
17
|
+
@mime_type = mime_type || detect_mime_type(filename)
|
|
18
|
+
@meta_type = meta_type
|
|
19
|
+
@description = description || filename
|
|
20
|
+
validate!
|
|
21
|
+
end
|
|
22
|
+
# rubocop:enable Metrics/ParameterLists
|
|
23
|
+
|
|
24
|
+
def size
|
|
25
|
+
content&.bytesize || 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def encoded_content
|
|
29
|
+
Base64.strict_encode64(content)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.build_soap_element(attachment)
|
|
33
|
+
attachment = from_hash(attachment) if attachment.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
'isds:dmMimeType' => attachment.mime_type,
|
|
37
|
+
'isds:dmFileMetaType' => attachment.meta_type,
|
|
38
|
+
'isds:dmFileDescr' => attachment.description,
|
|
39
|
+
'isds:dmEncodedContent' => attachment.encoded_content
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.from_hash(hash)
|
|
44
|
+
new(
|
|
45
|
+
filename: hash[:filename] || hash[:file_path]&.then { |p| File.basename(p) },
|
|
46
|
+
file_path: hash[:file_path],
|
|
47
|
+
content: hash[:content],
|
|
48
|
+
mime_type: hash[:mime_type],
|
|
49
|
+
meta_type: hash[:meta_type] || 'main',
|
|
50
|
+
description: hash[:description]
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate!
|
|
57
|
+
raise AttachmentError, 'Content is required' if content.nil? || content.empty?
|
|
58
|
+
raise FileSizeExceededError, 'File exceeds maximum size of 20MB' if size > MAX_FILE_SIZE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def detect_mime_type(filename)
|
|
62
|
+
types = MIME::Types.type_for(filename)
|
|
63
|
+
types.first&.content_type || 'application/octet-stream'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ISDS
|
|
4
|
+
module Authentication
|
|
5
|
+
class AccessInterface < Base
|
|
6
|
+
def apply(savon_options)
|
|
7
|
+
savon_options[:basic_auth] = [configuration.username, configuration.password]
|
|
8
|
+
savon_options[:headers] = (savon_options[:headers] || {}).merge(
|
|
9
|
+
'X-ISDS-Access-Interface' => '1'
|
|
10
|
+
)
|
|
11
|
+
savon_options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?
|
|
15
|
+
!configuration.username.nil? && !configuration.username.empty? &&
|
|
16
|
+
!configuration.password.nil? && !configuration.password.empty?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ISDS
|
|
4
|
+
module Authentication
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :configuration
|
|
7
|
+
|
|
8
|
+
def initialize(configuration)
|
|
9
|
+
@configuration = configuration
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def apply(savon_options)
|
|
13
|
+
raise NotImplementedError, "#{self.class}#apply must be implemented"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?
|
|
17
|
+
raise NotImplementedError, "#{self.class}#valid? must be implemented"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.build(configuration)
|
|
21
|
+
case configuration.auth_method
|
|
22
|
+
when :basic
|
|
23
|
+
Basic.new(configuration)
|
|
24
|
+
when :certificate
|
|
25
|
+
Certificate.new(configuration)
|
|
26
|
+
when :access_interface
|
|
27
|
+
AccessInterface.new(configuration)
|
|
28
|
+
else
|
|
29
|
+
raise ConfigurationError, "Unknown auth method: #{configuration.auth_method}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ISDS
|
|
4
|
+
module Authentication
|
|
5
|
+
class Basic < Base
|
|
6
|
+
def apply(savon_options)
|
|
7
|
+
savon_options[:basic_auth] = [configuration.username, configuration.password]
|
|
8
|
+
savon_options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def valid?
|
|
12
|
+
!configuration.username.nil? && !configuration.username.empty? &&
|
|
13
|
+
!configuration.password.nil? && !configuration.password.empty?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module ISDS
|
|
6
|
+
module Authentication
|
|
7
|
+
class Certificate < Base
|
|
8
|
+
def apply(savon_options)
|
|
9
|
+
savon_options[:ssl_cert_file] = configuration.certificate_path
|
|
10
|
+
savon_options[:ssl_cert_key_file] = configuration.private_key_path
|
|
11
|
+
savon_options[:ssl_cert_key_password] = configuration.key_password if configuration.key_password
|
|
12
|
+
savon_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def valid?
|
|
16
|
+
certificate_exists? && key_exists? && certificate_readable?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def certificate
|
|
20
|
+
@certificate ||= OpenSSL::X509::Certificate.new(File.read(configuration.certificate_path))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def expires_at
|
|
24
|
+
certificate.not_after
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expired?
|
|
28
|
+
expires_at < Time.now
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def subject_info
|
|
32
|
+
certificate.subject.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.from_p12(p12_path, password)
|
|
36
|
+
p12 = OpenSSL::PKCS12.new(File.read(p12_path), password)
|
|
37
|
+
{ certificate: p12.certificate, key: p12.key }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def certificate_exists?
|
|
43
|
+
!configuration.certificate_path.nil? && File.exist?(configuration.certificate_path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def key_exists?
|
|
47
|
+
!configuration.private_key_path.nil? && File.exist?(configuration.private_key_path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def certificate_readable?
|
|
51
|
+
OpenSSL::X509::Certificate.new(File.read(configuration.certificate_path))
|
|
52
|
+
true
|
|
53
|
+
rescue OpenSSL::X509::CertificateError, Errno::ENOENT
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|