triannon 2.0.1 → 3.0.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
  SHA1:
3
- metadata.gz: ebfe9c918d54957dd9840f3cca19729075b1f0e9
4
- data.tar.gz: aa9007f6543d8604a0be0a0b876c33e65be2168d
3
+ metadata.gz: 8bbca5b2eeb46df56a16905386aa0896e32edc1d
4
+ data.tar.gz: 4057efb53bd40de5dd20a637b0e6eeaa581e3274
5
5
  SHA512:
6
- metadata.gz: 55ce6508e866549664f1c359cb311d479054319377fad1f8af2a17dcea20dc097b2a3b1e30f016e4cea33900d76c4477c31e2803d0822f4410b350e2a1ece7a1
7
- data.tar.gz: 492c6fac232ae72b213ebc339bc9b519c4b39b4ca22ccd8e8724e7ecbd764209519524b9232dc3b08022ed4067184f88ecb016eb1870abcfab01131097292bd1
6
+ metadata.gz: ade73d1212e2d5ef3fb815946279af50e58a018999ebb0b1c7d8c8db9a5f56d85ddb4a42d9472d5c142e2ea1e1079646889fac21859afe61370ca47c85dae552
7
+ data.tar.gz: db831b37c53215465bb6441ea599dca56722c467a9968c477e7b2a0780d2070c7d8d2b717be6b645a1bff23d1c5a0ceb70578294c05a5535e5d3dd11057b2e22
@@ -1,4 +1,132 @@
1
1
  module Triannon
2
2
  class ApplicationController < ActionController::Base
3
+
4
+ before_action :authorize
5
+
6
+ #--- Authentication methods
7
+ #
8
+ # The #access_token_data method is generally available to the
9
+ # application. The #access_token_generate and #access_token_validate?
10
+ # methods are consolidated here to provide a unified view of these methods,
11
+ # although the application generally may not need to call them. They are
12
+ # used specifically in the auth_controller and these methods are tested
13
+ # in the auth_controller_spec.
14
+
15
+ # construct and encrypt an access token, using login data
16
+ # save the token into session[:access_token]
17
+ def access_token_generate(data)
18
+ timestamp = Time.now.to_i.to_s # seconds since epoch
19
+ salt = SecureRandom.base64(64)
20
+ key = ActiveSupport::KeyGenerator.new(timestamp).generate_key(salt)
21
+ crypt = ActiveSupport::MessageEncryptor.new(key)
22
+ session[:access_data] = [timestamp, salt]
23
+ session[:access_token] = crypt.encrypt_and_sign([data, timestamp])
24
+ end
25
+
26
+ # decrypt, parse and validate access token
27
+ def access_token_valid?(code)
28
+ begin
29
+ if code == session[:access_token]
30
+ identity, salt = session[:access_data]
31
+ key = ActiveSupport::KeyGenerator.new(identity).generate_key(salt)
32
+ crypt = ActiveSupport::MessageEncryptor.new(key)
33
+ data, timestamp = crypt.decrypt_and_verify(code)
34
+ elapsed = Time.now.to_i - timestamp.to_i # sec since token was issued
35
+ return data if elapsed < Triannon.config[:access_token_expiry]
36
+ end
37
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
38
+ # This is an invalid code, so return nil (a falsy value).
39
+ end
40
+ end
41
+
42
+ # Extract access login data from Authorization header, if it is valid.
43
+ # @param headers [Hash] request.headers with 'Authorization'
44
+ # @return login_data [Hash|nil]
45
+ def access_token_data(headers)
46
+ auth = headers['Authorization']
47
+ unless auth.nil? || auth !~ /^Bearer/
48
+ token = auth.split.last
49
+ access_token_valid?(token)
50
+ end
51
+ end
52
+
53
+
54
+ private
55
+
56
+ def authorize
57
+ # Require authorization on POST and DELETE requests.
58
+ auth_methods = ['POST','DELETE']
59
+ return true unless auth_methods.include? request.method
60
+ # Allow any requests to the /auth paths; provided that an
61
+ # anno root container cannot start with 'auth' in the name
62
+ # (which is controlled by the routes constraints).
63
+ return true if request.path =~ /^\/auth/
64
+ authorized_workgroup?
65
+ end
66
+
67
+ def authorized_workgroup?
68
+ # If the request does not map to a configured container, allow access.
69
+ container_auth = container_authorization
70
+ return true if container_auth.empty?
71
+ if request.headers['Authorization'].nil?
72
+ render401
73
+ return false
74
+ end
75
+ # Identify an intersection of the user and the authorized workgroups.
76
+ container_groups = container_auth['workgroups'] || []
77
+ match = container_groups & user_workgroups
78
+ if match.empty?
79
+ render403
80
+ false
81
+ else
82
+ true
83
+ end
84
+ end
85
+
86
+ # Extract container authorization from the configuration parameters
87
+ # @return authorization [Hash]
88
+ def container_authorization
89
+ container_config = request_container_config
90
+ if container_config.instance_of? Hash
91
+ container_config['auth'] || {}
92
+ else
93
+ {}
94
+ end
95
+ end
96
+
97
+ # Map the request root container to config parameters;
98
+ # assume that a request can only map to one root container.
99
+ # If this mapping fails, assume that authorization is OK.
100
+ def request_container_config
101
+ # TODO: refine the matching algorithm, esp if there is more than one
102
+ # match rather than assume it can only match one container.
103
+ request_container = params['anno_root']
104
+ configs = Triannon.config[:ldp]['anno_containers']
105
+ configs[request_container]
106
+ end
107
+
108
+ # Extract user workgroups from the access token
109
+ # @return workgroups [Array<String>]
110
+ def user_workgroups
111
+ access_data = access_token_data(request.headers)
112
+ if access_data.instance_of? Hash
113
+ access_data['workgroups'] || []
114
+ else
115
+ []
116
+ end
117
+ end
118
+
119
+ def render401
120
+ respond_to do |format|
121
+ format.all { head :unauthorized }
122
+ end
123
+ end
124
+
125
+ def render403
126
+ respond_to do |format|
127
+ format.all { head :forbidden }
128
+ end
129
+ end
130
+
3
131
  end
4
132
  end
@@ -0,0 +1,436 @@
1
+ require_dependency "triannon/application_controller"
2
+
3
+ module Triannon
4
+ # Adapted from http://image-auth.iiif.io/api/image/2.1/authentication.html
5
+ class AuthController < ApplicationController
6
+ include RdfResponseFormats
7
+
8
+ # HTTP request methods accepted by /auth/login
9
+ # TODO: enable GET when triannon supports true user authentication
10
+ LOGIN_ACCEPT = 'OPTIONS, POST'
11
+
12
+ # OPTIONS /auth/login
13
+ def options
14
+ # The request MUST use HTTP OPTIONS
15
+ case request.request_method
16
+ when 'OPTIONS'
17
+ if cookies[:login_user]
18
+ info = service_info_logout
19
+ else
20
+ info = service_info_login
21
+ end
22
+ # TODO: include optional info, such as service_info_client_identity
23
+ json_response(info, 200)
24
+ else
25
+ # The routes should prevent any execution here.
26
+ request_method_error(LOGIN_ACCEPT)
27
+ end
28
+ end
29
+
30
+ # POST to /auth/login
31
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#login-service
32
+ def login
33
+ # The service must set a Cookie for the Access Token Service to retrieve
34
+ # to determine the user information provided by the authentication system.
35
+ case request.request_method
36
+ when 'POST'
37
+ login_handler_post
38
+ else
39
+ # The routes should prevent any execution here.
40
+ request_method_error(LOGIN_ACCEPT)
41
+ end
42
+ end
43
+
44
+ # GET /auth/logout
45
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#logout-service
46
+ def logout
47
+ case request.request_method
48
+ when 'GET'
49
+ cookies.delete(:login_user)
50
+ reset_session
51
+ redirect_to root_url, notice: 'Successfully logged out.'
52
+ else
53
+ # The routes should prevent any execution here.
54
+ request_method_error('GET')
55
+ end
56
+ end
57
+
58
+ # POST /auth/client_identity
59
+ # A request MUST carry a body with:
60
+ # { "clientId" : "ID", "clientSecret" : "SECRET" }
61
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#client-identity-service
62
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#error-conditions
63
+ # return json body [String] containing: { "authorizationCode": code }
64
+ def client_identity
65
+ return unless process_post?
66
+ return unless process_json?
67
+ data = JSON.parse(request.body.read)
68
+ required_fields = ['clientId', 'clientSecret']
69
+ identity = parse_identity(data, required_fields)
70
+ if identity['clientId'] && identity['clientSecret']
71
+ if authorized_client? identity
72
+ id = identity['clientId']
73
+ pass = identity['clientSecret']
74
+ code = { authorizationCode: auth_code_generate(id, pass) }
75
+ json_response(code, 200)
76
+ else
77
+ err = {
78
+ error: 'invalidClient',
79
+ errorDescription: 'Invalid client credentials',
80
+ errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
81
+ }
82
+ json_response(err, 401)
83
+ end
84
+ else
85
+ err = {
86
+ error: 'invalidClient',
87
+ errorDescription: 'Insufficient client data for authentication',
88
+ errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
89
+ }
90
+ json_response(err, 401)
91
+ end
92
+ end
93
+
94
+
95
+ # GET /auth/access_token
96
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#access-token-service
97
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#error-conditions
98
+ def access_token
99
+ # The cookie established via the login service must be passed to this
100
+ # service. The service should delete the cookie from the login service
101
+ # and create a new cookie that allows the user to access content.
102
+ if session[:login_data]
103
+ if session[:client_data]
104
+ # When an authorization code was obtained using /auth/client_identity,
105
+ # that code must be passed to the Access Token Service as well.
106
+ auth_code = params[:code]
107
+ if auth_code.nil?
108
+ auth_code_required
109
+ elsif auth_code_valid?(auth_code)
110
+ access_token_granted
111
+ else
112
+ auth_code_invalid
113
+ end
114
+ else
115
+ # Without an authentication code, a login session is sufficient for
116
+ # granting an access token. However, the only way to enable a login
117
+ # session is for an authorized client to provide user data in POST
118
+ # /auth/login, which requires the client to first obtain an
119
+ # authentication code. Hence, this block of code should never get
120
+ # executed (unless login requirements change).
121
+ access_token_granted
122
+ end
123
+ else
124
+ login_required
125
+ end
126
+ end
127
+
128
+ # GET /auth/access_validate
129
+ # Authorize access based on validating an access token
130
+ def access_validate
131
+ auth = request.headers['Authorization']
132
+ if auth.nil? || auth !~ /Bearer/
133
+ access_token_invalid
134
+ else
135
+ token = auth.split[1]
136
+ if access_token_valid?(token)
137
+ response.status = 200
138
+ render nothing: true
139
+ else
140
+ access_token_invalid
141
+ end
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ # --------------------------------------------------------------------
148
+ # Access tokens
149
+
150
+ # Grant an access token for authorized access
151
+ def access_token_granted
152
+ cookies.delete(:login_user)
153
+ login_data = session.delete(:login_data)
154
+ access_token_generate(login_data) # saves to session[:access_token]
155
+ data = {
156
+ accessToken: session[:access_token],
157
+ tokenType: 'Bearer',
158
+ expiresIn: Triannon.config[:access_token_expiry]
159
+ }
160
+ json_response(data, 200)
161
+ end
162
+
163
+ # Issue a 401 to challenge for a client access token
164
+ def access_token_invalid
165
+ err = {
166
+ error: 'invalidAccess',
167
+ errorDescription: 'invalid access token',
168
+ errorUri: ''
169
+ }
170
+ json_response(err, 401)
171
+ end
172
+
173
+
174
+ # --------------------------------------------------------------------
175
+ # User authentication
176
+
177
+ # Handles POST /auth/login
178
+ # The request MUST include a URI parameter 'code=client_token' where
179
+ # the 'client_token' has been obtained from /auth/client_identity and
180
+ # the request MUST carry a body with the following JSON template:
181
+ # { "userId" : "ID", "workgroups" : "wgA, wgB" }
182
+ # Note that the current 'SearchWorks' requirements do not specify
183
+ # a 'userSecret' (it's not available).
184
+ def login_handler_post
185
+ return unless process_post?
186
+ return unless process_json?
187
+ auth_code = params[:code]
188
+ if auth_code.nil?
189
+ auth_code_required
190
+ elsif auth_code_valid?(auth_code)
191
+ begin
192
+ data = JSON.parse(request.body.read)
193
+ required_fields = ['userId', 'workgroups']
194
+ identity = parse_identity(data, required_fields)
195
+ # When an authorized client POSTs user data, it is simply accepted.
196
+ if identity['userId'] && identity['workgroups']
197
+ # Coerce workgroups into an Array.
198
+ wg = identity['workgroups'] || []
199
+ wg = wg.split(',') if wg.instance_of? String
200
+ wg.delete_if {|e| e.empty? }
201
+ identity['workgroups'] = wg
202
+ # Save the login_data until an access token is requested.
203
+ # Note that session data must be JSON compatible.
204
+ identity.to_json # check JSON compatibility
205
+ cookies[:login_user] = identity['userId']
206
+ session[:login_data] = identity
207
+ redirect_to root_url, notice: 'Successfully logged in.'
208
+ else
209
+ login_required
210
+ end
211
+ rescue
212
+ login_required(422)
213
+ end
214
+ else
215
+ auth_code_invalid
216
+ end
217
+ end
218
+
219
+ def login_required(status=401)
220
+ if request.format == :html
221
+ request_http_basic_authentication
222
+ elsif request.format == :json
223
+ # response.headers["WWW-Authenticate"] = %(Basic realm="Application")
224
+ if status == 401
225
+ err = {
226
+ error: '401 Unauthorized',
227
+ errorDescription: 'login credentials required',
228
+ errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
229
+ }
230
+ end
231
+ if status == 422
232
+ err = {
233
+ error: '422 Unprocessable Entity',
234
+ errorDescription: 'login credentials cannot be parsed',
235
+ errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
236
+ }
237
+ end
238
+ json_response(err, status)
239
+ end
240
+ end
241
+
242
+
243
+ # --------------------------------------------------------------------
244
+ # Client authentication
245
+
246
+ # Authenticates known clients
247
+ # @param identity [Hash] with fields 'clientId' and 'clientSecret'
248
+ def authorized_client?(identity)
249
+ @clients ||= Triannon.config[:authorized_clients]
250
+ @clients[identity['clientId']] == identity['clientSecret']
251
+ end
252
+
253
+ # --------------------------------------------------------------------
254
+ # Authentication tokens
255
+
256
+ # construct and encrypt an authorization code
257
+ def auth_code_generate(id, pass)
258
+ identity = "#{id};;;#{pass}"
259
+ timestamp = Time.now.to_i.to_s # seconds since epoch
260
+ salt = SecureRandom.base64(64)
261
+ key = ActiveSupport::KeyGenerator.new(identity).generate_key(salt)
262
+ crypt = ActiveSupport::MessageEncryptor.new(key)
263
+ session[:client_data] = [identity, salt]
264
+ session[:client_token] = crypt.encrypt_and_sign([id, timestamp])
265
+ end
266
+
267
+ # decrypt, parse and validate authorization code
268
+ def auth_code_valid?(code)
269
+ begin
270
+ if code == session[:client_token]
271
+ identity, salt = session[:client_data]
272
+ key = ActiveSupport::KeyGenerator.new(identity).generate_key(salt)
273
+ crypt = ActiveSupport::MessageEncryptor.new(key)
274
+ data, timestamp = crypt.decrypt_and_verify(code)
275
+ elapsed = Time.now.to_i - timestamp.to_i # sec since code was issued
276
+ return data if elapsed < Triannon.config[:client_token_expiry]
277
+ end
278
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
279
+ # This is an invalid code, so return nil (a falsy value).
280
+ end
281
+ end
282
+
283
+ # Issue a 403 for invalid client authorization codes
284
+ def auth_code_invalid
285
+ err = {
286
+ error: 'invalidClient',
287
+ errorDescription: 'Unable to validate authorization code',
288
+ errorUri: ''
289
+ }
290
+ json_response(err, 403)
291
+ end
292
+
293
+ # Issue a 401 to challenge for a client authorization code
294
+ def auth_code_required
295
+ err = {
296
+ error: 'invalidClient',
297
+ errorDescription: 'authorization code is required',
298
+ errorUri: ''
299
+ }
300
+ json_response(err, 401)
301
+ end
302
+
303
+
304
+ # --------------------------------------------------------------------
305
+ # Service information data
306
+ # TODO: evaluate whether we need this data
307
+
308
+ # return uri [String] the configured Triannon host URI
309
+ def service_base_uri
310
+ uri = RDF::URI.new(Triannon.config[:triannon_base_url])
311
+ uri.to_s.sub(uri.path,'')
312
+ end
313
+
314
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#access-token-service
315
+ # return info [Hash] access token service information
316
+ def service_info_access_token
317
+ {
318
+ service: {
319
+ "@id" => service_base_uri + '/auth/access_token',
320
+ "profile" => "http://iiif.io/api/image/2/auth/token",
321
+ "label" => "Request Access Token for Triannon"
322
+ }
323
+ }
324
+ end
325
+
326
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#client-identity-service
327
+ # return info [Hash] client identity service information
328
+ def service_info_client_identity
329
+ {
330
+ service: {
331
+ "@id" => service_base_uri + '/auth/client_identity',
332
+ "profile" => "http://iiif.io/api/image/2/auth/clientId"
333
+ }
334
+ }
335
+ end
336
+
337
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#login-service
338
+ # return info [Hash] login service information
339
+ def service_info_login
340
+ {
341
+ service: {
342
+ "@id" => service_base_uri + '/auth/login',
343
+ "profile" => "http://iiif.io/api/image/2/auth/login",
344
+ "label" => "Login to Triannon"
345
+ }
346
+ }
347
+ end
348
+
349
+ # http://image-auth.iiif.io/api/image/2.1/authentication.html#logout-service
350
+ # return info [Hash] logout service information
351
+ def service_info_logout
352
+ {
353
+ service: {
354
+ "@id" => service_base_uri + '/auth/logout',
355
+ "profile" => "http://iiif.io/api/image/2/auth/logout",
356
+ "label" => "Logout of Triannon"
357
+ }
358
+ }
359
+ end
360
+
361
+
362
+ # --------------------------------------------------------------------
363
+ # Utility methods
364
+
365
+
366
+ # @param data [Hash] Hash.to_json is rendered
367
+ # @param status [Integer] HTTP status code
368
+ def json_response(data, status)
369
+ render json: data.to_json, content_type: json_type_accepted, status: status
370
+ end
371
+
372
+ # Response content type to match an HTTP accept type for JSON formats
373
+ def json_type_accepted
374
+ mime_type_from_accept(['application/json', 'text/x-json', 'application/jsonrequest'])
375
+ end
376
+
377
+ # Parse POST JSON data to ensure it contains required fields
378
+ # @param fields [Array<String>] an array of required fields
379
+ def parse_identity(data, fields)
380
+ identity = Hash[fields.map {|f| [f, nil]}]
381
+ if fields.map {|f| data.key? f }.all?
382
+ fields.each {|f| identity[f] = data[f] }
383
+ end
384
+ identity
385
+ end
386
+
387
+ # Is the request content type JSON? If not, issue a 415 error.
388
+ def process_json?
389
+ if request.headers['Content-Type'] =~ /json/
390
+ true
391
+ else
392
+ logger.debug "Rejected Content-Type: #{request.headers['Content-Type']}"
393
+ render nothing: true, status: 415
394
+ false
395
+ end
396
+ end
397
+
398
+ # Is the request method POST? If not, issue a 405 error.
399
+ def process_post?
400
+ if request.post?
401
+ true
402
+ else
403
+ logger.debug "Rejected Request Method: #{request.request_method}"
404
+ err = {
405
+ error: 'invalidRequest',
406
+ errorDescription: "#{request.path} accepts POST requests, not #{request.request_method}",
407
+ errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
408
+ }
409
+ response.headers.merge!({'Allow' => 'POST'})
410
+ json_response(err, 405)
411
+ false
412
+ end
413
+ end
414
+
415
+ # @param accept [String] a csv for request methods accepted
416
+ def request_method_error(accept)
417
+ logger.debug "Rejected Request Method: #{request.request_method}"
418
+ response.status = 405
419
+ response.headers.merge!(Allow: accept)
420
+ respond_to do |format|
421
+ format.json {
422
+ err = {
423
+ error: 'invalidRequest',
424
+ errorDescription: "#{request.path} accepts: #{accept}",
425
+ errorUri: 'http://image-auth.iiif.io/api/image/2.1/authentication.html'
426
+ }
427
+ render json: err.to_json, content_type: json_type_accepted
428
+ }
429
+ format.html {
430
+ render nothing: true
431
+ }
432
+ end
433
+ end
434
+
435
+ end # AuthController
436
+ end # Triannon
@@ -40,7 +40,7 @@ module Triannon
40
40
  # Instance Methods ----------------------------------------------------------------
41
41
 
42
42
  def save
43
- _run_save_callbacks do
43
+ run_callbacks :save do
44
44
  # TODO: check if valid anno?
45
45
  @id = Triannon::LdpWriter.create_anno(self, root_container) if graph && graph.size > 2
46
46
  # reload from storage to get the anno id within the graph
@@ -51,7 +51,7 @@ module Triannon
51
51
  end
52
52
 
53
53
  def destroy
54
- _run_destroy_callbacks do
54
+ run_callbacks :destroy do
55
55
  Triannon::LdpWriter.delete_anno "#{root_container}/#{id}"
56
56
  end
57
57
  end
@@ -1,5 +1,14 @@
1
1
  Triannon::Engine.routes.draw do
2
2
 
3
+ # Authentication routes; these must precede '/:anno_root/*' and they
4
+ # preclude the use of an anno root named 'auth'.
5
+ match '/auth/login', to: 'auth#options', via: [:options]
6
+ match '/auth/login', to: 'auth#login', via: [:post]
7
+ get '/auth/logout', to: 'auth#logout'
8
+ get '/auth/access_token', to: 'auth#access_token'
9
+ get '/auth/access_validate', to: 'auth#access_validate'
10
+ post '/auth/client_identity', to: 'auth#client_identity'
11
+
3
12
  # 1. can't use resourceful routing because of :anno_root (dynamic path segment)
4
13
 
5
14
  # 2. couldn't figure out how to exclude specific values with regexp constraint since beginning and end regex matchers
@@ -8,75 +17,49 @@ Triannon::Engine.routes.draw do
8
17
  # get -> new action
9
18
  get '/annotations/:anno_root/new', to: 'annotations#new'
10
19
  get '/:anno_root/new', to: 'annotations#new',
11
- constraints: lambda { |request|
12
- anno_root = request.path_parameters[:anno_root]
13
- id = request.path_parameters[:id]
14
- anno_root !~ /^annotations$/ && anno_root !~ /^search$/ && anno_root !~ /^new$/ && id !~ /^search$/ && id !~ /^new$/
15
- }
20
+ constraints: lambda { |r| anno_root_filter(r) }
16
21
 
17
22
  # get -> search controller find action
18
23
  get '/annotations/:anno_root/search', to: 'search#find'
19
24
  get '/annotations/search', to: 'search#find'
20
25
  get '/:anno_root/search', to: 'search#find',
21
- constraints: lambda { |request|
22
- anno_root = request.path_parameters[:anno_root]
23
- anno_root !~ /^annotations$/ && anno_root !~ /^search$/ && anno_root !~ /^new$/
24
- }
26
+ constraints: lambda { |r| anno_root_filter(r) }
25
27
  get '/search', to: 'search#find'
26
28
 
27
- # get w id -> show action
29
+ # get + id -> show action
28
30
  get '/annotations/:anno_root/:id(.:format)', to: 'annotations#show',
29
- constraints: lambda { |request|
30
- anno_root = request.path_parameters[:anno_root]
31
- anno_root !~ /^search$/ && anno_root !~ /^new$/
32
- }
31
+ constraints: lambda { |r| anno_root_filter(r) && id_filter(r) }
33
32
  get '/:anno_root/:id(.:format)', to: 'annotations#show',
34
- constraints: lambda { |request|
35
- anno_root = request.path_parameters[:anno_root]
36
- id = request.path_parameters[:id]
37
- anno_root !~ /^annotations$/ && anno_root !~ /^search$/ && anno_root !~ /^new$/ && id !~ /^search$/ && id !~ /^new$/
38
- }
33
+ constraints: lambda { |r| anno_root_filter(r) && id_filter(r) }
39
34
 
40
- # get no id -> index action
35
+ # get - id -> index action
41
36
  get '/annotations/:anno_root', to: 'annotations#index',
42
- constraints: lambda { |request|
43
- anno_root = request.path_parameters[:anno_root]
44
- anno_root !~ /^search$/ && anno_root !~ /^new$/
45
- }
37
+ constraints: lambda { |r| anno_root_filter(r) }
46
38
  get '/:anno_root', to: 'annotations#index',
47
- constraints: lambda { |request|
48
- anno_root = request.path_parameters[:anno_root]
49
- anno_root !~ /^annotations$/ && anno_root !~ /^search$/ && anno_root !~ /^new$/
50
- }
39
+ constraints: lambda { |r| anno_root_filter(r) }
51
40
 
52
41
  # post -> create action
53
42
  post '/annotations/:anno_root', to: 'annotations#create',
54
- constraints: lambda { |request|
55
- anno_root = request.path_parameters[:anno_root]
56
- id = request.path_parameters[:id]
57
- anno_root !~ /^search$/ && anno_root !~ /^new$/ && id !~ /^search$/ && id !~ /^new$/
58
- }
43
+ constraints: lambda { |r| anno_root_filter(r) }
59
44
  post '/:anno_root', to: 'annotations#create',
60
- constraints: lambda { |request|
61
- anno_root = request.path_parameters[:anno_root]
62
- anno_root !~ /^annotations$/ && anno_root !~ /^search$/ && anno_root !~ /^new$/
63
- }
45
+ constraints: lambda { |r| anno_root_filter(r) }
64
46
 
65
47
  # delete -> destroy action
66
48
  delete '/annotations/:anno_root/:id(.:format)', to: 'annotations#destroy',
67
- constraints: lambda { |request|
68
- anno_root = request.path_parameters[:anno_root]
69
- id = request.path_parameters[:id]
70
- anno_root !~ /^search$/ && anno_root !~ /^new$/ && id !~ /^search$/ && id !~ /^new$/
71
- }
49
+ constraints: lambda { |r| anno_root_filter(r) && id_filter(r) }
72
50
  delete '/:anno_root/:id(.:format)', to: 'annotations#destroy',
73
- constraints: lambda { |request|
74
- anno_root = request.path_parameters[:anno_root]
75
- anno_root !~ /^annotations$/ && anno_root !~ /^search$/ && anno_root !~ /^new$/
76
- }
77
-
51
+ constraints: lambda { |r| anno_root_filter(r) && id_filter(r) }
78
52
 
79
53
  get '/annotations', to: 'search#find'
80
54
  root to: 'search#find'
81
55
 
82
56
  end
57
+
58
+
59
+ def anno_root_filter(request)
60
+ request.path_parameters[:anno_root] !~ /^annotations$|^auth$|^new$|^search$/
61
+ end
62
+
63
+ def id_filter(request)
64
+ request.path_parameters[:id] !~ /^new$|^search$/
65
+ end
@@ -1,3 +1,5 @@
1
+ # http://www.yaml.org/YAML_for_ruby.html
2
+
1
3
  development:
2
4
  ldp:
3
5
  url: http://your.ldp_store_url.here
@@ -7,32 +9,67 @@ development:
7
9
  # the container names here will also map to paths in the triannon url, e.g.
8
10
  # "foo" here will mean you add a foo anno by POST to http://your.triannon-server.com/annotations/foo
9
11
  # and you get the foo anno by GET to http://your.triannon-server.com/annotations/foo/(anno_uuid)
12
+ # auth: this configures access for POST and DELETE requests; when it is
13
+ # missing or the 'users' or 'workgroups' are empty, all access is OK.
14
+ # otherwise, the client must submit an access token containing data
15
+ # to be compared against these configuration parameters.
10
16
  anno_containers:
11
- - foo
12
- - blah
17
+ foo:
18
+ bar:
19
+ auth:
20
+ users: []
21
+ workgroups:
22
+ - org:wg-A
23
+ - org:wg-B
13
24
  solr_url: http://your.triannon_solr_url.here
14
25
  # triannon_base_url: the prefix for the urls for your annos
15
26
  triannon_base_url: http://your.triannon-server.com/annotations/
16
27
  max_solr_retries: 5
17
28
  base_sleep_seconds: 1
18
29
  max_sleep_seconds: 5
30
+ authorized_clients:
31
+ clientA: secretA
32
+ clientB: secretB
33
+ # expiry values are in seconds
34
+ client_token_expiry: 60
35
+ access_token_expiry: 3600
19
36
 
20
37
  test: &test
21
38
  ldp:
22
39
  url: http://your.ldp_store_url.here
23
40
  uber_container: anno
24
41
  anno_containers:
25
- - foo
26
- - blah
42
+ foo:
43
+ bar:
44
+ auth:
45
+ users: []
46
+ workgroups:
47
+ - org:wg-A
48
+ - org:wg-B
27
49
  solr_url: http://your.triannon_solr_url.here
28
50
  triannon_base_url: http://your.triannon-server.com/annotations/
51
+ authorized_clients:
52
+ clientA: secretA
53
+ clientB: secretB
54
+ # expiry values are in seconds
55
+ client_token_expiry: 60
56
+ access_token_expiry: 3600
29
57
 
30
58
  production:
31
59
  ldp:
32
60
  url: http://your.ldp_store_url.here
33
61
  uber_container: anno
34
62
  anno_containers:
35
- - foo
36
- - blah
63
+ foo:
64
+ bar:
65
+ auth:
66
+ users: []
67
+ workgroups:
68
+ - org:wg-A
69
+ - org:wg-B
37
70
  solr_url: http://your.triannon_solr_url.here
38
71
  triannon_base_url: http://your.triannon-server.com/annotations/
72
+ authorized_clients:
73
+ # expiry values are in seconds
74
+ client_token_expiry: 60
75
+ access_token_expiry: 3600
@@ -20,32 +20,59 @@ development:
20
20
  # "foo" here will mean you add a foo anno by POST to http://your.triannon-server.com/annotations/foo
21
21
  # and you get the foo anno by GET to http://your.triannon-server.com/annotations/foo/(anno_uuid)
22
22
  anno_containers:
23
- - foo
24
- - blah
23
+ foo:
24
+ bar:
25
+ auth:
26
+ users: []
27
+ workgroups:
28
+ - org:wg-A
29
+ - org:wg-B
25
30
  solr_url: http://localhost:8983/solr/triannon
26
31
  triannon_base_url: http://your.triannon-server.com/annotations/
27
32
  max_solr_retries: 5
28
33
  base_sleep_seconds: 1
29
34
  max_sleep_seconds: 5
35
+ authorized_clients:
36
+ clientA: secretA
37
+ clientB: secretB
38
+ # expiry values are in seconds
39
+ client_token_expiry: 60
40
+ access_token_expiry: 3600
30
41
  test: &test
31
42
  ldp:
32
43
  url: http://localhost:8983/fedora/rest
33
44
  uber_container: anno
34
45
  anno_containers:
35
- - foo
36
- - blah
46
+ foo:
47
+ bar:
48
+ auth:
49
+ users: []
50
+ workgroups:
51
+ - org:wg-A
52
+ - org:wg-B
37
53
  solr_url: http://localhost:8983/solr/triannon
38
54
  triannon_base_url: http://your.triannon-server.com/annotations/
55
+ authorized_clients:
56
+ clientA: secretA
57
+ clientB: secretB
58
+ # expiry values are in seconds
59
+ client_token_expiry: 60
60
+ access_token_expiry: 3600
39
61
  production:
40
62
  ldp:
41
63
  url:
42
64
  uber_container: anno
43
65
  anno_containers:
44
- - foo
45
- - blah
66
+ foo:
67
+ bar:
68
+ auth:
69
+ users: []
70
+ workgroups:
71
+ - org:wg-A
72
+ - org:wg-B
46
73
  solr_url:
47
74
  triannon_base_url:
48
- YML
75
+ YML
49
76
  create_file 'config/triannon.yml', default_yml
50
77
  end
51
78
 
@@ -40,8 +40,10 @@ namespace :triannon do
40
40
  puts "ERROR: Triannon config file missing: #{Triannon.triannon_file} - are you in the rails app root directory?"
41
41
  raise "Triannon config file missing: #{Triannon.triannon_file}"
42
42
  end
43
- Triannon.config[:ldp]['anno_containers'].each { |container_name|
44
- Triannon::LdpWriter.create_basic_container(Triannon.config[:ldp]['uber_container'], container_name)
43
+ uber_container = Triannon.config[:ldp]['uber_container']
44
+ containers = Triannon.config[:ldp]['anno_containers'].keys
45
+ containers.each { |c|
46
+ Triannon::LdpWriter.create_basic_container(uber_container, c.dup)
45
47
  }
46
48
  end
47
49
  end # namespace triannon
@@ -1,3 +1,3 @@
1
1
  module Triannon
2
- VERSION = "2.0.1"
2
+ VERSION = "3.0.0"
3
3
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: triannon
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naomi Dushay
8
8
  - Willy Mene
9
+ - Darren Weber
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2015-06-12 00:00:00.000000000 Z
13
+ date: 2015-07-22 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: rails
@@ -235,6 +236,48 @@ dependencies:
235
236
  - - ">="
236
237
  - !ruby/object:Gem::Version
237
238
  version: '0'
239
+ - !ruby/object:Gem::Dependency
240
+ name: pry
241
+ requirement: !ruby/object:Gem::Requirement
242
+ requirements:
243
+ - - ">="
244
+ - !ruby/object:Gem::Version
245
+ version: '0'
246
+ type: :development
247
+ prerelease: false
248
+ version_requirements: !ruby/object:Gem::Requirement
249
+ requirements:
250
+ - - ">="
251
+ - !ruby/object:Gem::Version
252
+ version: '0'
253
+ - !ruby/object:Gem::Dependency
254
+ name: pry-doc
255
+ requirement: !ruby/object:Gem::Requirement
256
+ requirements:
257
+ - - ">="
258
+ - !ruby/object:Gem::Version
259
+ version: '0'
260
+ type: :development
261
+ prerelease: false
262
+ version_requirements: !ruby/object:Gem::Requirement
263
+ requirements:
264
+ - - ">="
265
+ - !ruby/object:Gem::Version
266
+ version: '0'
267
+ - !ruby/object:Gem::Dependency
268
+ name: pry-rails
269
+ requirement: !ruby/object:Gem::Requirement
270
+ requirements:
271
+ - - ">="
272
+ - !ruby/object:Gem::Version
273
+ version: '0'
274
+ type: :development
275
+ prerelease: false
276
+ version_requirements: !ruby/object:Gem::Requirement
277
+ requirements:
278
+ - - ">="
279
+ - !ruby/object:Gem::Version
280
+ version: '0'
238
281
  - !ruby/object:Gem::Dependency
239
282
  name: rubocop
240
283
  requirement: !ruby/object:Gem::Requirement
@@ -321,6 +364,7 @@ files:
321
364
  - app/controllers/concerns/rdf_response_formats.rb
322
365
  - app/controllers/triannon/annotations_controller.rb
323
366
  - app/controllers/triannon/application_controller.rb
367
+ - app/controllers/triannon/auth_controller.rb
324
368
  - app/controllers/triannon/search_controller.rb
325
369
  - app/helpers/triannon/application_helper.rb
326
370
  - app/models/triannon/annotation.rb
@@ -372,7 +416,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
372
416
  version: '0'
373
417
  requirements: []
374
418
  rubyforge_project:
375
- rubygems_version: 2.4.3
419
+ rubygems_version: 2.4.8
376
420
  signing_key:
377
421
  specification_version: 4
378
422
  summary: Rails engine for working with OpenAnnotations stored in Fedora4