Coverage report: /development/source/library/org/datagraph/spocq-shard/src/core/authorization.lisp
| Kind | Covered | All | % |
| expression | 294 | 1248 | 23.6 |
| branch | 38 | 150 | 25.3 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;;; -*- Mode: Lisp; Syntax: Common-Lisp; Package: org.datagraph.spocq.implementation; -*-
3
(in-package :org.datagraph.spocq.implementation)
5
(:documentation "This file defines authorization operators for the 'org.datagraph.spocq' RDF engine."
8
"Copyright 2014 [james anderson](mailto:james.anderson@setf.de).")
11
"Implement authorization determinations for user/account/repository access based on acl assertions
12
stored in an account's system repository. The schema follows the w3c proposal[1], but includes also
13
negative assertions - that is noAccessTo in addition to accessTo
15
The pertinent resource authorizations are
16
/accounts/:account : control - for repository creation and deletion
17
/accounts/:account/authorization : read, write
18
/accounts/:account/configuration : read, write
19
/accounts/:account/configuration/:property : read, write
20
/accounts/:account/profile : read, write
21
/:account/:repository : read, write - for data access
22
/accounts/:account/repositories/:repository/authorization : read, write
23
/accounts/:account/repositories/:repository/configuration : read, write
24
/accounts/:account/repositories/:repository/configuration/:property : read, write
25
/accounts/:account/repositories/:repository/profile : read, write
27
But for the :repository/configuration -- :repository/configuration/:property combination,
28
each is a distinct resource.
29
The distinction between the resource which holds the authorization information and
30
the target is implemented by delegating to the resource-instance for the authorization list,
31
while permitting the resource to specify the pertinent resource-identifier for the access target
32
to be tested with respect to the authorizations.
34
authorization combines with scripts to add options to control access to the request and response streams.
35
for example, requests from an account could remote access to the request content and limit the source for
36
queries to a specific view url or a specific repository.
38
[1] : http://www.w3.org/wiki/WebAccessControl
42
(defun system-agent ()
44
(setq *system-agent* (make-instance 'administrator :name "system" :location "127.0.0.1"))))
46
(defgeneric task-access-mode (task)
48
(:method ((task task))
49
(if (operation-read-only-p task) |acl|:|Read| |acl|:|Write|))
50
(:method ((task task))
51
(task-operation-access-mode task (task-operation task))))
53
(defgeneric task-operation-access-mode (task operation)
54
(:method ((task query) (operation symbol))
55
(if (member operation '(update nil) :test #'string-equal)
56
|acl|:|Write| |acl|:|Read|)))
59
(defgeneric api-authorized-p (authorized-mode operation-mode)
60
(:method ((authorized t) (operation t))
61
(log-warn "api-authorized-p: invalid combination: ~s ~s" authorized operation)
63
(:method ((authorized list) (operation t))
64
(loop for mode in authorized
65
when (api-authorized-p mode operation)
68
(:method ((authorized (eql :control)) (operation t))
69
(api-authorized-p |http://www.w3.org/ns/auth/acl#|:|Control| operation))
70
(:method ((authorized (eql :write)) (operation t))
71
(api-authorized-p |http://www.w3.org/ns/auth/acl#|:|Write| operation))
72
(:method ((authorized (eql :read)) (operation t))
73
(api-authorized-p |http://www.w3.org/ns/auth/acl#|:|Read| operation))
74
(:method ((authorized (eql :read-write)) (operation t))
75
(api-authorized-p '(|http://www.w3.org/ns/auth/acl#|:|Write|
76
|http://www.w3.org/ns/auth/acl#|:|Read|)
78
(:method ((authorized (eql :read-write-control)) (operation t))
79
(api-authorized-p '(|http://www.w3.org/ns/auth/acl#|:|Write|
80
|http://www.w3.org/ns/auth/acl#|:|Read|
81
|http://www.w3.org/ns/auth/acl#|:|Control|)
84
(:method ((authorized t) (operation (eql :write)))
85
(api-authorized-p authorized |http://www.w3.org/ns/auth/acl#|:|Write|))
86
(:method (authorized (operation (eql :read)))
87
(api-authorized-p authorized |http://www.w3.org/ns/auth/acl#|:|Read|))
89
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Read|))
90
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Read|)))
92
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Read|))
93
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Write|)))
95
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Read|))
96
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Control|)))
98
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Write|))
99
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Read|)))
101
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Write|))
102
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Control|)))
104
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Write|))
105
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Write|)))
107
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Control|))
108
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Control|)))
110
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Control|))
111
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Read|)))
113
(:method ((authorized (eql |http://www.w3.org/ns/auth/acl#|:|Control|))
114
(operation (eql |http://www.w3.org/ns/auth/acl#|:|Write|)))
117
(defgeneric identifier-in-realm-p (resource-identifier realm-identifier)
118
(:method ((id1 null) (id2 t))
120
(:method ((id1 t) (id2 null))
122
(:method ((id1 null) (id2 null))
124
;; nb. no support for target lists as, since the acl is sorted, each entry must have a single target
125
(:method ((resource-identifier spocq:iri) (realm-identifier t))
126
(identifier-in-realm-p (spocq:iri-lexical-form resource-identifier) realm-identifier))
127
(:method ((resource-identifier t) (realm-identifier spocq:iri))
128
(identifier-in-realm-p resource-identifier (spocq:iri-lexical-form realm-identifier)))
130
(:method ((resource-identifier string) (realm-identifier string))
131
(log-debug "identifier-in-realm-p: ~s ~s" resource-identifier realm-identifier)
132
(and (<= (length realm-identifier) (length resource-identifier))
133
(string= realm-identifier resource-identifier :end2 (length realm-identifier))))
135
(:method ((resource identified-object) (realm t))
136
(identifier-in-realm-p (capability-identifier resource) realm)))
138
(defun compare-authorization-realms (one other)
139
(cond ((equalp one other)
141
((identifier-in-realm-p one other)
143
(t ;; (identifier-in-realm-p other one)
146
(defgeneric compare-authorization-agent-ids (one other)
147
(:documentation "return -/0/+ depending on whether the first argument agent id is in the realm of
148
the second, or the relation is inverted.")
149
(:method ((one agent) (other t))
150
(compare-authorization-agent-ids (agent-identifier one) other))
151
(:method ((one t) (other agent))
152
(compare-authorization-agent-ids one (agent-identifier other)))
153
(:method ((one authorization) (other t))
154
(compare-authorization-agent-ids (authorization-agent-id one) other))
155
(:method ((one t) (other authorization))
156
(compare-authorization-agent-ids one (authorization-agent-id other)))
158
(:method ((one t) (other t))
159
(cond ((equalp one other)
161
((identifier-in-realm-p one other)
163
((identifier-in-realm-p other one)
166
(defgeneric compare-authorization-modes (one other)
167
(:method ((one list) (other list))
169
do (ecase (compare-authorization-modes one other)
170
((-1 0) (setf other (remove one other)))
172
finally (return (if (null other) 0 -1))))
173
(:method ((one symbol) (other list))
175
(if (rest other) -1 0)
177
(:method ((one symbol) (other null))
179
(:method ((one symbol) (other symbol))
180
(if (eq one other) 0 1)))
182
(and (eql 0 (compare-authorization-modes '(|acl|:|Read| |acl|:|Write|) '(|acl|:|Read| |acl|:|Write|)))
183
(eql -1 (compare-authorization-modes '(|acl|:|Read| ) '(|acl|:|Read| |acl|:|Write|)))
184
(eql 1 (compare-authorization-modes '(|acl|:|Read| |acl|:|Write|) '(|acl|:|Read| ))))
187
(defgeneric url-authorization-url (target)
188
(:method ((target t)) ;; interned iri -> symbols
190
(:method ((target spocq:http-url))
191
(let* ((lexical-form (spocq:url-lexical-form target))
192
(type (when lexical-form (http:resource-path-type lexical-form))))
194
(intern-iri (subseq lexical-form 0 (- (length lexical-form) (1+ (length type)))))
197
(defparameter *access-authorized-p.cache-false* nil)
200
#+(or) (defmethod ACCESS-AUTHORIZED-P ((repository service-repository) (agent rdfcache-account) (mode (eql |http://www.w3.org/ns/auth/acl#|:|Read|))) t)
201
(defgeneric access-authorized-p (target agent action)
203
"Iff the agent is authorized to perform the action on the target, return true.
205
The control flow involves two levels:
206
- an outer level handles cached capability lists per agent
207
- a second level resolves targets to identifiers
208
- the base level extracts the (target x agent x mode) authorization relation from the acl graph.
210
Logic to cache the results for an individual (target x agent x mode) combination
211
resolves the target to the iri lexical form
213
The method for administrators supersedes everything to permit universal access.")
215
(:argument-precedence-order agent target action)
217
(:method :around ((target t) (agent administrator) (action t))
218
"Override all other evaluation to indicate access is allowed"
221
(:method :around ((target authorized-resource) (agent agent) (action t))
222
"Check for and reuse the status for a known agent respective the resource.
223
Iff none exists, compute and cache it."
224
;;(format *error-output* "~%~a" (hunchentoot::get-backtrace))
225
(let* ((target-namestring (iri-lexical-form (capability-identifier target)))
226
(capability (cons target-namestring action)))
227
(multiple-value-bind (determination known-p)
228
(agent-capability agent capability)
229
;; check for a known determination for the agent-action combination
233
;; if none is known, derive and cache the specific result
234
(setf determination (call-next-method))
235
(when (or determination *access-authorized-p.cache-false*)
236
(setf (agent-capability agent capability) determination))
237
(log-debug "access-authorized-p: newly cached ~s ~s ~s ~s" agent determination action target)))
238
(log-notice "access-authorized-p: ~s ~s ~s ~s" agent determination action target)
241
(:method ((target account) (agent agent) (action t))
242
(or (eql (agent-account agent) target)
243
(not (control-account-access target))
244
(query-capability (instance-repository-id target) agent target action)))
246
(:method ((target repository) (agent agent) (action t))
247
(or (eql (agent-account agent) (repository-account target))
248
(not (control-repository-access (repository-account target)))
249
(query-capability (instance-repository-id target) agent target action)))
251
(:method ((target view) (agent agent) (action t))
252
(or (eql (agent-account agent) (view-account target))
253
(not (control-view-access (view-account target)))
254
(query-capability (instance-repository-id (view-repository target)) agent target action)))
256
(:method ((target spocq:url) (agent agent) (action t))
257
"given an arbitrary resource identifier, attempt to locate its metadata from the the authorization:"
258
(let* ((path (split-string (puri:uri-path (puri:uri target)) "/"))
259
(account-name (first path))
260
(repository-name (second path))
261
(authorization-target (url-authorization-url target))
263
(cond ((equal (agent-name agent) account-name))
264
((account-exists-p account-name)
265
(let ((account (account account-name)))
266
(or (eql (agent-account agent) account)
267
(query-capability (instance-repository-id account) agent authorization-target action
269
:repository-id (compute-repository-identifier account-name repository-name))
272
(query-capability (system-repository) agent authorization-target action)
274
(log-notice "access-authorized-p: ~s ~s ~s ~s" agent determination action target)
277
(:method ((target repository) (agent account) (action t))
278
(or (not (control-repository-access (repository-account target)))
279
(query-capability (instance-repository-id target) agent target action)))
281
(:method ((target repository) (agent view) (action t))
282
(or (not (control-repository-access (repository-account target)))
283
(query-capability (instance-repository-id target) agent target action)))
285
;; the default method denies access
286
(:method ((target t) (agent t) (action t))
289
(defgeneric access-authorization-authorizes-p (authorization resource agent action)
290
(:documentation "Used by access-authorized-p to test the requested mode against an authorization entry .")
291
(:method ((context t) (target t) (agent t) (action t))
293
(:method ((authorization authorization) (target authorized-resource) (agent agent) (action t))
294
"this, the base method for authorizations, tests that the action is specified."
295
(find action (authorization-access-mode authorization)))
297
(:method ((authorization anonymous-authorization) (target authorized-resource) (agent null) (action t))
298
(find action (authorization-access-mode authorization)))
300
(:method ((authorization location-authorization) (target authorized-resource) (agent agent) (action t))
301
(and (loop with location = (agent-location agent)
302
for predicate in (authorization-location-predicates authorization)
303
when (funcall predicate location)
307
(:method ((authorization authenticated-authorization) (target authorized-resource) (account account) (action t))
308
(agent-equal-p account (authorization-agent-id authorization)))
310
(:method ((authorization authenticated-authorization) (target authorized-resource) (agent agent) (action t))
311
"When authentication is required, if an id is specified, it must match.
312
Otherwise, permit a match by class."
313
(let ((authorization-agent-id (authorization-agent-id authorization)))
314
;; first, check if the agent matches
315
(and (case authorization-agent-id
316
((nil |urn:dydra|:|User|)
317
(case (authorization-agent-class authorization)
318
(|urn:dydra|:|LocatedAgent|
319
(agent-location agent))
320
((|urn:dydra|:|User| |urn:dydra|:|Account|)
321
(agent-identifier agent))))
322
(|http://xmlns.com/foaf/0.1/|:|Agent| ; anomalous
326
(log-debug "access-authorized-p: test agent: ~s ~s " (agent-identifier agent) authorization-agent-id)
327
(agent-equal-p agent authorization-agent-id))))
329
(log-debug "access-authorized-p: agent matched ~s " agent)
331
;; continue, to test the action
332
(call-next-method)))))
335
(defgeneric anonymous-authorization-p (object)
336
(:method ((object t)) nil)
337
(:method ((object anonymous-authorization)) t)
338
(:method ((authorization-list authorization-list))
339
(some #'anonymous-authorization-p (authorization-list-controls authorization-list))))
341
(defgeneric anonymous-access-authorized-p (target)
342
(:documentation "Return true iff anonymous access is, in general permitted,
343
that is, without the an agent and without respect to the action.")
344
(:method ((authorization-list authorization-list))
345
(some #'anonymous-authorization-p (authorization-list-controls authorization-list)))
346
(:method ((target authorized-resource))
347
(let ((acl-list (resource-authorization-list target)))
348
(when acl-list (anonymous-access-authorized-p acl-list)))))
352
(defgeneric resource-authorization-list (resource)
353
(:method ((resource authorized-resource))
354
(or (get-resource-authorization-list resource)
355
(let ((*agent* (system-agent)))
356
(setf-resource-authorization-list (read-resource (make-resource-authorization-list resource)) resource)))))
358
(defgeneric make-resource-authorization-list (resource &rest args)
359
(:method ((resource authorized-resource) &rest args)
360
(declare (dynamic-extent args))
361
(apply #'make-instance 'authorization-list
366
(defgeneric combine-resource-authorization-lists (base restrictions)
367
(:method :around ((l1 t) (l2 t))
371
(:method ((base list) (restrictions list))
372
(flet ((more-restrictive (restriction)
373
(loop for base-authorization in base
374
when (authorization-less-p restriction base-authorization)
376
(declare (dynamic-extent #'more-restrictive))
377
(append (remove-if-not #'more-restrictive restrictions) base)))
378
(:method ((base authorization-list) (restrictions t))
379
(combine-resource-authorization-lists (authorization-list-controls base) restrictions))
380
(:method ((base t) (restrictions authorization-list))
381
(combine-resource-authorization-lists base (authorization-list-controls restrictions))))
383
;;; (compute-resource-authorization-list "james/test")
386
(defgeneric authorization-less-p (one-authorization other-authorization)
387
(:documentation "return true iff the one autorization applies to a proper subset of agents
388
and allows a proper subset of privileges compared to the other.")
389
(:method ((one authorization) (other authorization))
390
(flet ((compare-realms (r1 r2)
392
(compare-authorization-realms r1 r2)
394
(let ((to-one (authorization-access-to one))
395
(to-other (authorization-access-to other))
396
(not-to-one (authorization-no-access-to one))
397
(not-to-other (authorization-no-access-to other)))
398
(or (and not-to-one to-other (ecase (compare-realms not-to-one to-other)
399
((-1 0) t) ((1 nil) nil)))
400
(and (ecase (compare-realms to-one to-other)
401
((-1 0) t) ((1 nil) nil))
402
(ecase (compare-realms not-to-one not-to-other)
403
((-1 0) t) ((1 nil) nil))
404
(ecase (compare-authorization-agent-ids one other)
405
((-1 0) t) ((1 nil) nil))
406
(ecase (compare-authorization-modes (authorization-access-mode one) (authorization-access-mode other))
407
((-1 0) t) ((1 nil) nil))))))))
411
(defmethod encode-presentation-graph ((authorization-list authorization-list))
412
;;; convert(c s p o) to (s p o)
413
(mapcar #'rest (authorization-list-statements authorization-list)))
416
(defmethod decode-presentation-graph ((authorization-list authorization-list) (state (eql |urn:dydra|:|default|)))
417
"Recognize the 'default' keyword to reset the authorization list to just minimal
418
access for the owner."
419
(setf (authorization-list-statements authorization-list)
420
(loop for (nil s p o c)
421
in (compute-initial-resource-authorization-graph (authorization-list-resource authorization-list))
422
collect (list c s p o)))
425
(defmethod decode-presentation-graph ((authorization-list authorization-list) (state cons))
426
(let* ((identifier (instance-identifier authorization-list))
428
(cspo-statements (loop for statement in state
429
for (s p o) = statement
430
when (member p '(|urn:dydra|:|User|))
431
do (push o agent-ids)
432
collect (cons identifier statement))))
433
#+(or) ; do not constraint the agent ids
434
(with-open-transaction (*system-repository-id*)
435
(let ((unknown-agents (loop for id in agent-ids
436
unless (repository-match-field *transaction* |urn:dydra|:|default| id '?::p '?::o)
438
(assert (null unknown-agents) ()
439
"Agents are not known: ~{~s~^, ~}" unknown-agents)))
440
(setf (authorization-list-statements authorization-list) cspo-statements)
443
(defmethod decode-presentation-graph ((authorization-list authorization-list) (state t))
444
(error 'type-error :datum state :expected-type '(or cons (eql |urn:dydra|:|default|))))
447
(defmethod unbind-resource ((resource authorization-list))
448
(setf-authorization-list-statements nil resource)
449
(setf (authorization-list-controls resource) nil)
450
(setf (instance-store-graph resource) nil)
453
(defmethod encode-store-graph ((authorization-list authorization-list))
454
;; use the cached statements directly
455
(authorization-list-statements authorization-list))
457
(defmethod decode-store-graph ((authorization-list authorization-list) (acl-statements list))
458
;; decode the graph and cache it for round-trips
459
(setf (authorization-list-statements authorization-list) acl-statements)
460
(setf-instance-store-graph acl-statements authorization-list)
465
;;; coordinate statements and authorization controls
467
(defmethod (setf authorization-list-statements) ((statements list) (authorization-list authorization-list))
468
(setf-authorization-list-statements statements authorization-list)
469
(setf (authorization-list-controls authorization-list)
470
(compute-graph-authorization-controls statements authorization-list)))
474
;;; graph <-> authorizations
477
(defmethod decode-json-object ((object authorization-list) (json-data t) &rest args)
478
"Decode the json data as autorizations - either a list of or a single one.
479
Provide the slots definitions for the concrete authorization classes as the
481
(declare (dynamic-extent args))
482
(apply #'decode-json-object (instance-predicates object) json-data args))
485
;;; (identifier-in-realm-p <http://dydra.com/account/configuration> <http://dydra.com/account>)
486
;;; (identifier-in-realm-p <http://dydra.com/account> <http://dydra.com/account/configuration>)
488
(defgeneric compute-graph-authorization-controls (graph authorization-list)
489
(:documentation "Extract the resource's authorization-relevant metadata and
490
translate it into authorization controls.")
492
(:method ((statements list) (authorization-list authorization-list))
493
;; given the specification as a (c s p o) field
494
;; compute the declared authentication controls using all assertions
495
;; which include the identified resource as the accessTo target
496
;; this allows to construct the list form a cbd in which the resource may
497
;; appear as an agent as well as the target.
498
(let* ((cache (make-hash-table :test 'eq))
499
(identifier (instance-identifier authorization-list)))
500
(loop for (nil subject predicate object) in statements
502
(|acl|:|accessTo| ; allow multiples
503
(push object (getf (gethash subject cache) :access-to)))
504
(|acl|:|noAccessTo| ; allow multiples
505
(push object (getf (gethash subject cache) :no-access-to)))
507
(setf (getf (gethash subject cache) :agent-id) object)
508
(setf (getf (gethash subject cache) :agent-class) |urn:dydra|:|User|))
510
(setf (getf (gethash subject cache) :agent-class) object)
513
(setf (getf (gethash subject cache) :agent-class) object)
514
(let ((class (some #'(lambda (stmt)
515
(when (and (eq (second stmt) object) (eq (third stmt) |rdf|:|type|))
518
(locations (mapcar #'fourth
519
(remove-if-not #'(lambda (stmt)
520
(and (eq (second stmt) object)
521
(eq (third stmt) |sioc|:|ip_address|)))
523
(setf (getf (gethash subject cache) :agent-class) class)
524
(setf (getf (gethash subject cache) :locations) locations))))
526
(push object (getf (gethash subject cache) :locations)))
528
(push object (getf (gethash subject cache) :access-mode)))
529
(t ; skip everything else
531
(sort (loop for initargs being each hash-value of cache
532
for target-resource-ids = (getf initargs :access-to)
533
for target-resource-ids-not = (getf initargs :no-access-to)
534
for class = (if (getf initargs :agent-id)
535
(if (getf initargs :locations)
536
'authenticated-location-authorization
537
'authenticated-authorization)
538
(case (getf initargs :agent-class)
539
((nil |foaf|:|Agent|)
540
(if (getf initargs :locations)
541
'anonymous-location-authorization
542
'anonymous-authorization))
544
(if (getf initargs :locations)
545
'authenticated-location-authorization
546
'authenticated-authorization))))
547
if (or target-resource-ids target-resource-ids-not)
548
append (loop for access-to in target-resource-ids
549
if (and (eq class 'anonymous-authorization)
550
(not (equal (getf initargs :access-mode) '(|http://www.w3.org/ns/auth/acl#|:|Read|))))
551
do (log-warn "excessive anonymous access: ~s . ~s" access-to initargs)
552
else collect (apply #'make-instance class :access-to access-to initargs)
555
and append (loop for no-access-to in target-resource-ids-not
556
collect (apply #'make-instance class :no-access-to no-access-to initargs))
557
else do (log-warn "Anomalous authorization for resource: ~s, ~s" identifier initargs))
559
:key #'(lambda (authorization) (length (spocq:iri-lexical-form (or (authorization-access-to authorization)
560
(authorization-no-access-to authorization)))))))))
562
(defgeneric compute-initial-resource-authorization-graph (resource)
563
(:method ((account account))
564
(compute-initial-account-authorization-graph (account-name account)))
566
(:method ((repository repository))
567
(compute-initial-repository-authorization-graph (account-name (repository-account repository)) (repository-name repository))))
574
(defgeneric agent-authenticated-by-token (token &key if-does-not-exist location repository)
575
(:documentation "Given a token, if the agent is known, return the cached instance.
576
Otherwise, look of the agent identity from the persistent account information.
577
If it is known, instantiate and return the corrsponding agent.
578
If not, return nil or signal an error as specified by :if-does-not-exit")
580
(:method ((token string) &key (if-does-not-exist :error) (location nil)
581
(repository (system-repository)))
582
(or (gethash (list :token token) *users*)
583
(unless (or (null-sequence-p token) (null-sequence-p *system-repository-id*))
584
(multiple-value-bind (user-id admin-p user-name account-name)
585
(authenticate-user-token repository token)
592
:account account-name
594
(ecase if-does-not-exist
597
(error "Unknown token '~c...~c'" (char token 0) (char token (1- (length token))))))))
599
(:method ((token t) &key (if-does-not-exist :error) &allow-other-keys)
600
(ecase if-does-not-exist
603
(error "Invalid token '~s'" token)))))
606
(spocq:unbound-variable-p (SPOCQ:make-UNBOUND-VARIABLE NIL))
610
(agent-authenticated-by-token "sXQeb11KcCyoX6n9n90X" :if-does-not-exist nil))
615
;;; could rewrite this to cross (match ?account accessToken ,auth-token) with (match ,account ?prop ?value)
616
;;; and do the join by hand...
618
;;; (defmethod spocq.si::authenticate-user-token :before (repository auth-token) ) ;; (print spocq.i::*agent*))
620
(defparameter *authenticate-user-token.sparql*
625
(spocq.a:|graph| |urn:dydra|:|users|
627
(spocq.a:|triple| ?::user |urn:dydra|:|accessToken| ?::auth-token)
628
(spocq.a:|triple| ?::user |dc|:|title| ?::name)))
629
(spocq.a:|graph| |urn:dydra|:|users|
631
(spocq.a:|triple| ?::user |sioc|:|administrator_of| ?::isAdminOf))))
633
(spocq.a:|graph| |urn:dydra|:|accounts|
635
(spocq.a:|triple| ?::accountByOwner |acl|:|owner| ?::user)))
636
(spocq.a:|graph| ?::accountByOwner
638
(spocq.a:|triple| ?::accountByOwner |foaf|:|accountName| ?::accountNameByOwner)))))
640
(spocq.a:|graph| |urn:dydra|:|accounts|
642
(spocq.a:|triple| ?::accountbyToken |urn:dydra|:|accessToken| ?::auth-token)))
643
(spocq.a:|graph| ?::accountbyToken
645
(spocq.a:|triple| ?::accountbyToken |foaf|:|accountName| ?::accountNameByToken)))))
646
(?::user ?::isAdminOf ?::name ?::auth-token ?::accountNameByOwner ?::accountNameByToken))
647
"Locate user restricted by token, with addition information on the owned and any restricted account")
651
(defparameter *authenticate-user-token.query* nil)
652
(defun compute-authenticate-user-token-query (&rest args &key (id (make-internal-task-id) )
653
(agent (system-agent))
655
(unless *authenticate-user-token.query*
656
(setq *authenticate-user-token.query*
659
:sse-expression *authenticate-user-token.sparql*
660
:account (account "system")
661
:repository-id *system-repository-id*
663
:dynamic-bindings `((?::auth-token) ,"")) ; as a place holder
665
(with-task-environment (:task *authenticate-user-token.query*)
666
(compile-query *authenticate-user-token.query*)))
667
(apply #'clone-instance *authenticate-user-token.query* :id id
672
(defun run-authenticate-user-token-query (&rest args)
673
(let ((cl-user::*map-repository-statements-callback.verbose* t)
674
(*DATA-TRACE-OUTPUT* *trace-output*)
675
(*ALGEBRA-TRACE-OUTPUT* *trace-output*))
676
(apply #'run-sparql-internal *authenticate-user-token.sparql* args)))
677
(defun run-authenticate-user-token-query (&rest args)
678
(apply #'run-sparql-internal *authenticate-user-token.sparql* args))
679
(defun rerun-authenticate-user-token-query (&rest args)
680
(apply #'run-sparql-internal *authenticate-user-token.sparql* args))
682
(in-package :spocq.i)
684
(loop for i below 1000
685
do (multiple-value-bind (id admin? user account)
686
(authenticate-user-token (system-repository) "sXQeb11KcCyoX6n9n90X")
687
(unless (and (stringp user) (stringp account))
688
(break "query failed: ~s" (list id admin? user account)))))
691
(defparameter cl-user::*map-repository-statements-callback.verbose* nil)
693
(defgeneric authenticate-user-token (repository auth-token &key location)
694
(:method ((repository t) (auth-token t) &key location)
695
(declare (ignore location))
697
(:method ((repository-id string) (auth-token t) &rest args)
698
(declare (dynamic-extent args))
699
(apply #'authenticate-user-token (repository repository-id) auth-token args))
701
(:method ((repository repository) (auth-token string) &key location)
702
(declare (ignore location)) ; should eventually use that as an added contraint
703
;; nb. the method with a pre-compiled query fails unless it is cloned, which
704
;; eliminates much advantage, while adding complexity
705
(let ((results (run-authenticate-user-token-query :repository repository
706
:agent (system-agent)
707
:dynamic-bindings `((?::auth-token) ,auth-token))))
708
(when (consp results)
709
(destructuring-bind ((user-id is-admin user-name token account-by-owner account-by-token) &rest others) results
711
(log-warn "token authenticates multiple users. : ~s . : ~s"
712
(list user-id is-admin user-name token account-by-owner account-by-token)
714
(cond ((iri-p user-id)
715
(unless (stringp user-name)
716
(log-warn "no name in anomalous user token authentication results: ~s"
717
(list user-id is-admin user-name token account-by-owner account-by-token))
718
(let* ((iri-string (iri-lexical-form user-id))
719
(iri-name (first (last (split-string iri-string "/")))))
720
(setf user-name iri-name)))
721
(let ((is-admin-p (not (spocq:unbound-variable-p is-admin))))
725
(cond ((stringp account-by-owner) account-by-owner)
726
((stringp account-by-token) account-by-token)
729
(log-warn "no user-id in anomalous user token authentication results: ~s"
730
(list user-id is-admin user-name token account-by-owner account-by-token))
732
(let ((cl-user::*map-repository-statements-callback.verbose* t)
733
(*DATA-TRACE-OUTPUT* *trace-output*)
734
(*ALGEBRA-TRACE-OUTPUT* *trace-output*))
735
(log-warn "anomalous user token authentication results: ~s"
736
(list user-id is-admin user-name token account-by-owner account-by-token))
738
(log-warn "rerun token authentication results: ~s"
739
(rerun-authenticate-user-token-query :repository repository
740
:agent (system-agent)
741
:dynamic-bindings `((?::auth-token) ,auth-token))))
744
;;; (authenticate-user-token "system/system" "sXQeb11KcCyoX6n9n90X")
745
;;; (progn (room t) (dotimes (x 1000) (authenticate-user-token "system/system" "asdf")) (room t))
747
(run-sparql-internal "select ?user ?name ?token ?admin
749
{ { graph <urn:dydra:users>
750
{ ?user <urn:dydra:accessToken> ?token .
751
?user <http://purl.org/dc/elements/1.1/title> ?name } }
753
{ graph <urn:dydra:users>
754
{ ?user <http://rdfs.org/sioc/ns#administrator_of> ?admin } }
756
:repository-id "system/system"
757
:agent (system-agent))
759
(defun account-access-token (account-name &key (if-does-not-exist :error))
760
(let ((alternatives (run-sparql-internal `(spocq.a:|select|
762
(spocq.a:|graph| ?::account
764
(spocq.a:|triple| ?::account |http://xmlns.com/foaf/0.1/|:|accountName| ,account-name)
765
(spocq.a:|triple| ?::account |urn:dydra|:|accessToken| ?::|token|)
768
(spocq.a:|triple| ?::account |http://xmlns.com/foaf/0.1/|:|accountName| ,account-name)
769
(spocq.a:|triple| ?::account |urn:dydra|:|accessToken| ?::|token|)
771
(?::|token| ?::account))
772
:repository-id *system-repository-id*
773
:agent (system-agent))))
775
(when (rest alternatives)
776
;; error. it could yield an uintended user
777
(error "account-access-token: anomalous results for account ~s: ~s" account-name alternatives))
778
(destructuring-bind (token account) (first alternatives)
779
(values token account)))
781
(ecase if-does-not-exist
782
(:error (error "Account not found: ~s." account-name))
786
(defgeneric describe-token-resource (repository auth-token)
787
(:method ((repository-id string) (auth-token t))
788
(describe-token-resource (repository repository-id) auth-token))
789
(:method ((repository repository) (auth-token string))
790
(run-sparql-internal `(spocq.a:|select|
792
(spocq.a:|graph| ?::account
794
(spocq.a:|triple| ?::account |urn:dydra|:|accessToken| ,auth-token)
795
(spocq.a:|triple| ?::account ?::predicate ?::object)))
797
(spocq.a:|triple| ?::account |urn:dydra|:|accessToken| ,auth-token)
798
(spocq.a:|triple| ?::account ?::predicate ?::object)))
799
(?::account ?::predicate ?::object))
800
:repository repository
801
:agent (system-agent))))
806
(defparameter *authenticate-user-password.sparql*
810
(spocq.a:|graph| |urn:dydra|:|users|
812
(spocq.a:|triple| ?::user |urn:dydra|:|encryptedPassword| ?::encrypted)
813
(spocq.a:|triple| ?::user |dc|:|title| ?::name)))
814
(spocq.a:|graph| |urn:dydra|:|accounts|
816
(spocq.a:|triple| ?::account |acl|:|owner| ?::user)
817
(spocq.a:|triple| ?::account |foaf|:|accountName| ?::accountName))))
818
(spocq.a:|graph| |urn:dydra|:|users|
820
(spocq.a:|triple| ?::user |sioc|:|administrator_of| ?::isAdminOf))))
821
(?::user ?::isAdminOf ?::encrypted ?::name ?accountName)))
823
(defparameter *authenticate-user-password.query* nil)
824
(defun compute-authenticate-user-password-query (&rest args &key (id (make-internal-task-id) )
825
(agent (system-agent))
829
SELECT ?user ?account ?admin ?encrypted
830
WHERE { graph <urn:dydra:users>
831
{ ?user foaf:accountName 'user-name' .
832
?user <urn:dydra:passwordHash> ?encrypted . }
834
{ ?user sioc:administrator_of ?isAdminOf . }
837
with criteria as per http://php.net/manual/en/function.crypt.php"
838
(unless *authenticate-user-password.query*
839
(setq *authenticate-user-password.query*
841
:sse-expression *authenticate-user-password.sparql*
842
:account (account "system")
843
:repository-id *system-repository-id*
845
:dynamic-bindings `((?::name) "")))
846
(with-task-environment (:task *authenticate-user-password.query*)
847
(compile-query *authenticate-user-password.query*)))
848
(apply #'clone-instance *authenticate-user-password.query* :id id :agent agent args))
850
(defparameter +pepper+ "f26f93ae733730c6467f9b3fef6bf3eccf4c410a4b9be82ccc44315d6e143706ee99d6bfdb606fbd7c82aaf6810503ddd547bd13468f0395356df1abc7e17886")
852
(defgeneric authenticate-user-password (repository user-name password)
853
(:method ((repository t) (user-name t) (password t))
855
(:method ((repository-id string) (user-name t) (password t))
856
(authenticate-user-password (repository repository-id) user-name password))
857
(:method ((repository repository) (user-name string) (password string))
859
(let ((results (run-sparql-internal *authenticate-user-password.sparql*
860
:repository repository
861
:agent (system-agent)
862
:dynamic-bindings `((?::name) ,user-name))))
864
;; warn. the password constraint could miss the intended user
865
(log-warn "authenticate-user-password: anomalous results for name ~s: ~s" user-name results))
866
(loop for (user-id is-admin encrypted-pasword name account-name) in results
867
when (and (>= (length encrypted-pasword) 29)
868
(bcrypt:password= (concatenate 'string password +pepper+) encrypted-pasword))
869
return (values user-id
870
(not (spocq:unbound-variable-p is-admin))
872
(not (spocq:unbound-variable-p account-name)))))))
874
(defgeneric agent-authenticated-by-password (user-name password &key if-does-not-exist location)
875
(:documentation "Given a name/password pair, if the agent is known, return the cached instance.
876
Otherwise, look up the agent identity from the persistent account information.
877
If it is known, instantiate and return the corrsponding agent.
878
If not, return nil or signal an error as specified by :if-does-not-exit
880
If partial credentials are given, treat the given value as an auth token.
881
Note that 'token:' will likely appear in logs, while ':token' should not.")
883
(:method ((user-name string) (password string) &rest args &key (if-does-not-exist :error) (location nil))
884
(or (if (or (null-sequence-p password) (equalp password "X"))
885
(if (null-sequence-p user-name)
887
(apply #'agent-authenticated-by-token user-name args))
888
(if (null-sequence-p user-name)
889
(apply #'agent-authenticated-by-token password args)
890
(unless (null-sequence-p *system-repository-id*)
891
(or (gethash (list :password user-name password) *users*)
892
(multiple-value-bind (user-id admin-p _name_ account-name)
893
(authenticate-user-password (system-repository) user-name password)
894
(declare (ignore _name_))
900
:account account-name
901
:admin-p admin-p))))))
902
(ecase if-does-not-exist
905
(error "Unknown user/password '~a'" (unless (equalp password "X") user-name)))))))
910
(defparameter *authenticate-user-name.sparql*
915
(spocq.a:|graph| |urn:dydra|:|users|
917
(spocq.a:|triple| ?::user |dc|:|title| ?::name)))
918
(spocq.a:|graph| |urn:dydra|:|users|
920
(spocq.a:|triple| ?::user |sioc|:|administrator_of| ?::isAdminOf))))
921
(spocq.a:|graph| |urn:dydra|:|users|
923
(spocq.a:|triple| ?::user |urn:dydra|:|encryptedPassword| ?::encrypted))))
924
(spocq.a:|graph| |urn:dydra|:|users|
926
(spocq.a:|triple| ?::user |urn:dydra|:|accessToken| ?::auth-token))))
927
(?::user ?::isAdminOf ?::name ?::encrypted ?::auth-token))
928
"Locate the named user and whether there is either a password or a token associated with them and the admin status.")
931
(defparameter *authenticate-user-name.query* nil)
932
(defun compute-authenticate-user-name-query (&rest args &key (id (make-internal-task-id) )
933
(agent (system-agent))
937
SELECT ?user ?account ?admin ?encrypted
938
WHERE { graph <urn:dydra:users>
939
{ ?user foaf:accountName 'user-name' . }
941
{ ?user sioc:administrator_of ?isAdminOf . }
944
(unless *authenticate-user-name.query*
945
(setq *authenticate-user-name.query*
947
:sse-expression *authenticate-user-name.query*
948
:account (account "system")
949
:repository-id *system-repository-id*
951
:dynamic-bindings `((?::name) "")))
952
(with-task-environment (:task *authenticate-user-name.query*)
953
(compile-query *authenticate-user-name.query*)))
954
(apply #'clone-instance *authenticate-user-name.query* :id id :agent agent args))
956
(defparameter *authenticate-user-name.permit-privileges* nil
957
"allow authentication by name only even if additional auth data is present and/or the user is an admin.")
959
(defgeneric authenticate-user-name (repository user-name)
960
(:documentation "provide for the case, where some larger context has done the authentication
961
and it remains just to determine whether the user exists and what privledges the have")
962
(:method ((repository t) (user-name t))
964
(:method ((repository-id string) (user-name t))
965
(authenticate-user-name (repository repository-id) user-name))
966
(:method ((repository repository) (user-name string))
968
(let ((results (run-sparql-internal *authenticate-user-name.sparql*
969
:repository repository
970
:agent (system-agent)
971
:dynamic-bindings `((?::name) ,user-name))))
972
(when (consp results)
974
;; error. it could yield an uintended user
975
(error "authenticate-user-name: anomalous results for name ~s: ~s" user-name results))
976
(destructuring-bind (user-id is-admin name password token) (first results)
977
(when (or *authenticate-user-name.permit-privileges*
978
(and (spocq:unbound-variable-p is-admin)
979
(spocq:unbound-variable-p password)
980
(spocq:unbound-variable-p token)))
981
(values user-id nil name)))))))
983
(defgeneric agent-authenticated-by-name (user-name &key if-does-not-exist location)
984
(:documentation "Given a name, if the agent is known, return the cached instance.
985
Otherwise, look up the agent identity from the persistent account information.
986
If it is known, instantiate and return the corresponding agent.
987
If not, return nil or signal an error as specified by :if-does-not-exit")
989
(:method ((user-name string) &key (if-does-not-exist :error) (location nil))
990
(or (gethash (list :name user-name) *users*)
991
(and (not (null-sequence-p user-name))
992
(not (null-sequence-p *system-repository-id*))
993
(multiple-value-bind (user-id admin-p)
994
(authenticate-user-name (system-repository) user-name)
995
(declare (ignore admin-p))
997
(ensure-agent ; never admin by name
1000
:location location))))
1001
(ecase if-does-not-exist
1004
(error "Unknown user '~a'" user-name))))))
1009
(defgeneric authenticate-user-session (repository session-id)
1010
(:method ((repository t) (session-id t))
1012
(:method ((repository repository) (session-id string))
1013
;; 20170915: this version is speculative, as the current storage is in an lmdb database
1014
;; see store;lmdb;spocq.classes.lisp
1015
(let ((results (run-sparql
1019
(spocq.a:|graph| |urn:dydra|:|sessions|
1021
(spocq.a:|triple| ?::session |urn:dydra|:|session_id| ,session-id)
1022
(spocq.a:|triple| ?::session |urn:dydra|:|session_user| ?::user)))
1023
(spocq.a:|graph| |urn:dydra|:|users|
1025
(spocq.a:|triple| ?::user |dc|:|title| ?::name))))
1026
(spocq.a:|graph| |urn:dydra|:|users|
1028
(spocq.a:|triple| ?::user |sioc|:|administrator_of| ?::isAdminOf))))
1029
(?::user ?::isAdminOf ?name))
1030
:repository repository
1031
:agent (system-agent))))
1032
(when (consp results)
1033
(when (rest results)
1034
;; error. it could yield an uintended user
1035
(error "authenticate-user-session: anomalous results for session ~s: ~s" session-id results))
1036
(destructuring-bind (user-id is-admin user-name) (first results)
1037
(values user-id (not (spocq:unbound-variable-p is-admin)) user-name))))))
1040
(defgeneric agent-authenticated-by-session (session-id &key if-does-not-exist location)
1041
(:documentation "Given a session, if the session is known, return the cached instance.
1042
Otherwise, look for the session and, from that retrieve the user/account information
1043
If it is known, instantiate and return the corresponding agent.
1044
If not, return nil or signal an error as specified by :if-does-not-exit")
1046
(:method ((session-id string) &key (if-does-not-exist :error) (location nil))
1047
(or (gethash (list :session session-id) *users*)
1048
(unless (or (null-sequence-p session-id) (null-sequence-p *system-repository-id*))
1049
(multiple-value-bind (user-id admin-p user-name)
1050
(authenticate-user-session (system-repository) session-id)
1057
:admin-p admin-p))))
1058
(ecase if-does-not-exist
1061
(error "Unknown session '~a'" session-id))))))
1065
(dydra:sparql-query `(sparql:select (sparql:bgp (?::account |urn:dydra|:|encryptedPassword| ?::pw)
1066
(?::account |acl|:|owner| ?::user))
1068
:repository "system/system"
1069
:agent (dydra:system-agent))
1071
;;; nb. this does not include the escaping practice which is pesent in the php code, as the
1072
;;; store operations do not construct and then parse query expressions.
1077
(dydra:sparql-query `(sparql:select (sparql:bgp (?::account |urn:dydra|:|accessToken| ?::token)
1078
(?::account |acl|:|owner| ?::user))
1080
:repository "system/system"
1081
:agent (dydra:system-agent))
1084
(run-sparql-internal "select ?user ?name ?token ?admin
1086
{ { graph <urn:dydra:users>
1087
{ ?user <urn:dydra:accessToken> ?token .
1088
?user <http://purl.org/dc/elements/1.1/title> ?name } }
1090
{ graph <urn:dydra:users>
1091
{ ?user <http://rdfs.org/sioc/ns#administrator_of> ?admin } }
1093
:repository-id "system/system"
1094
:agent (system-agent))
1096
(authenticate-user-token (repository "system/system") "8v8Wy1ieBVo2JxMhNjnquOq9PUe4QEKLFBlmsTy73NaS6xHGF2EcjCYrozwz")
1099
(run-sparql-internal "select ?g ?s ?o where { {graph ?g { ?s <http://rdfs.org/sioc/ns#administrator_of> ?o } }
1101
{ ?s <http://rdfs.org/sioc/ns#administrator_of> ?o } }"
1102
:repository-id "system/system" :agent (system-agent))
1103
(authenticate-user-password "system/system" "james" *password*)
1105
(run-sparql-internal "drop graph <http://dydra.com/accounts/james>"
1106
:repository-id "system/system" :agent (system-agent))
1107
(run-sparql-internal "insert data { graph <urn:dydra:users> { <http://dydra.com/users/james> <http://rdfs.org/sioc/ns#administrator_of> <http://dydra.com> } }"
1108
:repository-id "system/system" :agent (system-agent) )
1110
(run-sparql-internal "select ?p where { ?s ?p2 'jhacker' . ?s ?p ?o }" :repository-id "system/system" :agent (system-agent))
1111
(loop for id in '(115632144 869 8779684 981) collect (rlmdb:term-number-value id))
1113
(<http://dydra.com/users/james> <http://rdfs.org/sioc/ns#administrator_of>
1114
<http://dydra.com> |urn:dydra|:|users|)
1116
(loop for id in '(981 115642369 869 8779684) collect (rlmdb:term-number-value id))
1118
(account-access-token "jhacker")
1119
(account-access-token "james")
1121
(defmethod (setf configuration-parameter) :after ((key string) (parameter (eql :api-key)))
1122
(setq *api-key* key))
1125
(run-sparql-internal `(spocq.a:|select|
1127
(spocq.a:|triple| ?::|s| |http://xmlns.com/foaf/0.1/|:|accountName| ?::name)
1130
:repository-id "system/system"
1131
:agent (system-agent))
1134
(run-sparql-internal `(spocq.a:|select|
1136
(spocq.a:|triple| ?::|s| |http://xmlns.com/foaf/0.1/|:|accountName| "jhacker")
1139
:repository-id "system/system"
1140
:agent (system-agent))
1143
(let ((authorization-assertions (run-sparql-internal "
1144
select ?node ?mode ?agent ?class
1146
graph ?repositoryURI {
1147
# ?aclNode |acl|:|accessTo| ?repositoryURI .
1148
?aclNode |acl|:|mode| ?mode.
1149
optional { ?aclNode |acl|:|agent| ?agent . }
1150
optional { ?aclNode |acl|:|agentClass| ?classNode .
1151
?classNode |rdf|:|type| ?type .
1152
?classNode ... location, authenticated, user, owner, ...
1155
;; for each node, create one authorization record for each agent/agent-class
1156
;; and accumulate the access mode into it. if it ia an agent class, reify those
1157
;; properties as an agent-class instance
1158
(loop for (node mode agent class) in authorization-assertions
1159
for args = (or (assoc node acl-initargs)
1160
(first (push list node
1161
when agent do (setf (getf (cddr args) :agent) agent)
1162
and when class do (setf (getf (cddr args) :class) class)
1165
(c2mop:finalize-inheritance (find-class 'authorization-list))
1166
(decode-json-object (allocate-instance (find-class 'authorization-list))
1167
(parse-json "[{\"ID\": \"1\", \"accessTo\": \"a\"},
1168
{\"ID\": \"2\", \"accessTo\": \"b\"}]"))
1173
;;; obsolete; replaced with core authorization logic
1174
(defgeneric get-resource-authorization (repository resource)
1175
(:method ((repository dydra:repository) (resource account-resource))
1176
"given some requested resource, use its _internal_ identifier to collect
1177
the authorization information. this is likely identical with the request-identifier, but
1178
may differ for multi-host sites."
1180
(let* ((id (dydra:instance-identifier resource))
1181
(query `(sparql:select
1183
(sparql:leftjoin (sparql:bgp (?::auth |acl|:|accessTo| ,id)
1184
(?::auth |acl|:|mode| ?::mode))
1185
(sparql:leftjoin (sparql:bgp (?::auth |acl|:|agent| ?::agent))
1186
(sparql:bgp (?::auth |acl|:|agentClass| ?::class)
1187
(?::class |sioc|:|ip_address| ?::ipAddress)))))
1188
(?::auth ?::mode ?::agent ?::ipAddress)))
1189
(results (dydra:sparql-query query :repository repository)))
1190
(when (consp results)
1191
(loop with authorizations = (make-hash-table :test #'equalp)
1192
for (auth-node mode agent ip-address) in results
1193
do (cond (ip-address
1194
(setf (getf (gethash auth-node authorizations) :class) 'ip-address-authorization
1195
(getf (gethash auth-node authorizations) |sioc|:|ip_address|) ip-address)
1196
(push (getf (gethash auth-node authorizations) |acl|:|mode|) mode))
1198
(setf (getf (gethash auth-node authorizations) :class) 'agent-authorization
1199
(getf (gethash auth-node authorizations) |acl|:|agent|) agent)
1200
(push (getf (gethash ip-address authorizations) |acl|:|mode|) mode)))
1201
finally (return (loop for authorization being each hash-value of authorizations
1202
collect (destructuring-bind (&key class mode ip-address agent) authorization
1203
(declare (ignore class))
1205
(make-instance 'agent-authorization :agent agent :mode mode))
1207
(make-instance 'ip-address-authorization :ip-address ip-address :mode mode)))))))))))
1211
;;; obsolete; replaced with a version without the context as the first argument
1212
(defgeneric access-authorized-p (context target agent action)
1214
"Iff the context permits the agent to perform the action on the target, return true.
1216
The control flow involves three levels:
1217
- the base level evaluate the respective (target agent mode) combination in the
1218
context of an individual authorization control
1219
- a collection level combines the controls in a list according to their +/- status
1220
and the result of the respective individual evaluation
1221
- the application api expects some request object as the context, from which an additional
1222
authorization list may be combined with that from the target.
1224
Logic is included to cache the results for an individual (resource agent) combination -
1225
but independent of context, in order to expedite the determination.
1227
- agent class (beyond authenticated, located, anonymous)
1230
The method for administrators supersedes everything to permit universal access.")
1232
(:argument-precedence-order agent target context action)
1234
(:method :around ((context t) (target t) (agent administrator) (action t))
1235
"Override all other evaluation to indicate access is allowed"
1238
(:method :around ((context t) (target authorized-resource) (agent agent) (action t))
1239
"Check for and reuse the status for a known agent respective the resource.
1240
Iff none exists, compute and cache it."
1242
(if *authorization-target*
1244
(let* ((*authorization-target* target)
1245
(target-namestring (iri-lexical-form (instance-identifier target)))
1246
(capability (cons target-namestring action)))
1247
(multiple-value-bind (determination known-p)
1248
(agent-capability agent capability)
1249
;; check for a known determination for the agent-action combination
1253
;; if none is known, derive and cache the specific result
1254
(setf determination (call-next-method))
1255
(setf (agent-capability agent capability) determination)))
1256
(log-debug "access-authorized-p: access cached ~s ~s ~s ~s" target agent action determination)
1259
(:method :around ((context authorization) (target authorized-resource) (agent t) (action t))
1260
(let ((result (call-next-method)))
1261
(log-debug "access-authorized-p: ~s ~s ~s ~s -> ~s"
1262
context target agent action result)
1265
(:method ((context task) (target authorized-resource) (agent t) (action t))
1266
(access-authorized-p (task-repository context) target agent action))
1268
(:method ((context authorized-resource) (target null) (agent t) (action t))
1269
(access-authorized-p (resource-authorization-list context) context agent action))
1270
(:method ((context t) (target authorized-resource) (agent t) (action t))
1271
(let ((a-list (resource-authorization-list target)))
1273
(access-authorized-p a-list target agent action))
1275
(log-warn "resource yields no authorizations: ~s" target)
1277
(:method ((context authorized-resource) (target authorized-resource) (agent t) (action t))
1278
"Determine access to the resource in a given context by testing against a combined
1279
authorization list which is that of the target reduced by that of the context."
1280
(let* ((context-list (resource-authorization-list context))
1281
(target-list (resource-authorization-list target))
1282
(a-list (combine-resource-authorization-lists target-list context-list)))
1284
(access-authorized-p a-list target agent action))
1286
(log-warn "resource yields no authorizations: ~s x ~s" context target)
1289
(:method ((context authorization-list) (target authorized-resource) (agent t) (action t))
1290
;; reduce the test to a search through the controls
1291
(let ((controls (authorization-list-controls context)))
1293
(access-authorized-p controls target agent action))))
1295
(:method ((context list) (target authorized-resource) (agent t) (action t))
1296
"Test the authorization sequence - as ordered most-specific-first, to select the
1297
first match for target-in-realm and return from that the authorization state for
1298
the intended action.
1299
Ambiguity/conflict is resolved such that the first authorization which matches by domain
1300
denies or allows - as per access-to / no-access-to, and if none matches, the presence of
1301
restrictions and absence of permissions indicates that the access is authorized."
1302
(loop with no-access-tested = nil
1303
with access-tested = nil
1304
for authorization in context
1305
for to = (authorization-access-to authorization)
1306
for not-to = (authorization-no-access-to authorization)
1308
(when (and not-to (identifier-in-realm-p target not-to))
1309
(if (access-authorization-authorizes-p authorization target agent action)
1311
(setf no-access-tested t)))
1312
(when (and to (identifier-in-realm-p target to))
1313
(if (access-authorization-authorizes-p authorization target agent action)
1315
(setf access-tested t))))
1316
finally (return (and (not access-tested) no-access-tested))))
1318
;; the base methods authorize an agent or anonymous for an anonymous authorization
1319
(:method ((context t) (target t) (agent t) (action t))
1322
(defgeneric access-authorization-authorizes-p (authorization resource agent action)
1323
(:documentation "Used by access-authorized-p to test the requested mode against an authorization entry .")
1324
(:method ((context t) (target t) (agent t) (action t))
1326
(:method ((authorization authorization) (target authorized-resource) (agent agent) (action t))
1327
"this, the base method for authorizations, tests that the action is specified."
1328
(find action (authorization-access-mode authorization)))
1330
(:method ((authorization anonymous-authorization) (target authorized-resource) (agent null) (action t))
1331
(find action (authorization-access-mode authorization)))
1333
(:method ((authorization location-authorization) (target authorized-resource) (agent agent) (action t))
1334
(and (loop with location = (agent-location agent)
1335
for predicate in (authorization-location-predicates authorization)
1336
when (funcall predicate location)
1338
(call-next-method)))
1340
(:method ((authorization authenticated-authorization) (target authorized-resource) (account account) (action t))
1341
(agent-equal-p account (authorization-agent-id authorization)))
1343
(:method ((authorization authenticated-authorization) (target authorized-resource) (agent agent) (action t))
1344
"When authentication is required, if an id is specified, it must match.
1345
Otherwise, permit a match by class."
1346
(let ((authorization-agent-id (authorization-agent-id authorization)))
1347
;; first, check if the agent matches
1348
(and (case authorization-agent-id
1349
((nil |urn:dydra|:|User|)
1350
(case (authorization-agent-class authorization)
1351
(|urn:dydra|:|LocatedAgent|
1352
(agent-location agent))
1353
((|urn:dydra|:|User| |urn:dydra|:|Account|)
1354
(agent-identifier agent))))
1355
(|http://xmlns.com/foaf/0.1/|:|Agent| ; anomalous
1359
(log-debug "access-authorized-p: test agent: ~s ~s " (agent-identifier agent) authorization-agent-id)
1360
(agent-equal-p agent authorization-agent-id))))
1362
(log-debug "access-authorized-p: agent matched ~s " agent)
1364
;; continue, to test the action
1365
(call-next-method)))))
1368
(defgeneric anonymous-authorization-p (object)
1369
(:method ((object t)) nil)
1370
(:method ((object anonymous-authorization)) t)
1371
(:method ((object authenticated-authorization))
1372
(eq (authorization-agent-id object) '|http://xmlns.com/foaf/0.1/|:|Agent|))
1373
(:method ((authorization-list authorization-list))
1374
(some #'anonymous-authorization-p (authorization-list-controls authorization-list))))
1376
(defgeneric anonymous-access-authorized-p (target)
1377
(:documentation "Return true iff anonymous access is, in general permitted,
1378
that is, without the an agent and without respect to the action.")
1379
(:method ((authorization-list authorization-list))
1380
(some #'anonymous-authorization-p (authorization-list-controls authorization-list)))
1381
(:method ((target authorized-resource))
1382
(let ((acl-list (resource-authorization-list target)))
1383
(when acl-list (anonymous-access-authorized-p acl-list)))))
1387
(defgeneric resource-authorization-list (resource)
1388
(:method ((resource authorized-resource))
1389
(or (get-resource-authorization-list resource)
1390
(let ((*agent* (system-agent)))
1391
(setf-resource-authorization-list (read-resource (make-resource-authorization-list resource)) resource)))))
1393
(defgeneric make-resource-authorization-list (resource &rest args)
1394
(:method ((resource authorized-resource) &rest args)
1395
(declare (dynamic-extent args))
1396
(apply #'make-instance 'authorization-list
1401
(defgeneric combine-resource-authorization-lists (base restrictions)
1402
(:method :around ((l1 t) (l2 t))
1405
(call-next-method)))
1406
(:method ((base list) (restrictions list))
1407
(flet ((more-restrictive (restriction)
1408
(loop for base-authorization in base
1409
when (authorization-less-p restriction base-authorization)
1411
(declare (dynamic-extent #'more-restrictive))
1412
(append (remove-if-not #'more-restrictive restrictions) base)))
1413
(:method ((base authorization-list) (restrictions t))
1414
(combine-resource-authorization-lists (authorization-list-controls base) restrictions))
1415
(:method ((base t) (restrictions authorization-list))
1416
(combine-resource-authorization-lists base (authorization-list-controls restrictions))))
1418
;;; (compute-resource-authorization-list "james/test")
1421
(defgeneric authorization-less-p (one-authorization other-authorization)
1422
(:documentation "return true iff the one autorization applies to a proper subset of agents
1423
and allows a proper subset of privileges compared to the other.")
1424
(:method ((one authorization) (other authorization))
1425
(flet ((compare-realms (r1 r2)
1427
(compare-authorization-realms r1 r2)
1429
(let ((to-one (authorization-access-to one))
1430
(to-other (authorization-access-to other))
1431
(not-to-one (authorization-no-access-to one))
1432
(not-to-other (authorization-no-access-to other)))
1433
(or (and not-to-one to-other (ecase (compare-realms not-to-one to-other)
1434
((-1 0) t) ((1 nil) nil)))
1435
(and (ecase (compare-realms to-one to-other)
1436
((-1 0) t) ((1 nil) nil))
1437
(ecase (compare-realms not-to-one not-to-other)
1438
((-1 0) t) ((1 nil) nil))
1439
(ecase (compare-authorization-agent-ids one other)
1440
((-1 0) t) ((1 nil) nil))
1441
(ecase (compare-authorization-modes (authorization-access-mode one) (authorization-access-mode other))
1442
((-1 0) t) ((1 nil) nil))))))))