triannon 2.0.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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