/
/access.py
1 """
2 implementation of http://esw.w3.org/WebAccessControl
3
4 currently this is contaminated with my old 'viewableBy' access system
5 and some other photo-site-specific things, but hopefully those can be
6 removed
7 """
8
9 import logging, random, time, datetime
10 from dateutil.tz import tzlocal
11 from rdflib import URIRef, RDF, Literal
12 from nevow import url
13 import auth
14 from genshi.template import TemplateLoader
15 from genshi.output import XHTMLSerializer
16 from edit import writeStatements
17 from imageurl import ImageSetDesc
18 from lib import print_timing
19 from ns import PHO, FOAF, ACL, DCTERMS
20 loader = TemplateLoader(".", auto_reload=True)
21 serializer = XHTMLSerializer()
22 log = logging.getLogger('access')
23
24 class NeedsMoreAccess(ValueError):
25 pass
26
27 def agentClassCheck(graph, agent, photo):
28 """
29 is there a perm that specifically lets this agent (or class) see this photo?
30 """
31 bb = {'initBindings' : {'photo' : photo, 'agent' : agent}}
32 # UNION was breaking
33 return (graph.queryd("ASK { ?photo pho:viewableByClass ?agent . }", **bb) or
34 graph.queryd("ASK { ?photo pho:viewableBy ?agent . }", **bb) or
35 graph.queryd("ASK { [ acl:agent ?agent ; acl:mode acl:Read ; acl:accessTo ?photo ] . }", **bb))
36
37 def viewableViaPerm(graph, uri, agent):
38 """
39 viewable via some permission that can be set and removed
40 """
41 log.debug("viewableViaPerm uri=%s agent=%s", uri, agent)
42 if (graph.contains((uri, PHO['viewableBy'], PHO['friends']))
43 or graph.contains((uri, PHO['viewableBy'], PHO['anyone']))):
44 log.debug("ok, graph says :viewableBy :friends or :anyone")
45 return True
46
47 if graph.queryd("ASK { [ acl:agent pho:friends ; acl:mode acl:Read ; acl:accessTo ?photo ] .}", initBindings={'photo' : uri}):
48 log.debug("ok, graph has an authorization for :friends")
49 return True
50
51 # spec wants me to use agentClass on this, but I've already
52 # screwed that up elsewhere, so it will take new triples to make
53 # the old data compatible
54 if graph.queryd("""ASK {
55 [ acl:agent foaf:Agent ;
56 acl:mode acl:Read ;
57 acl:accessTo ?photo ] .
58 }""", initBindings={'photo' : uri}):
59 log.debug("ok, graph has an authorization for foaf:Agent ('the public')")
60 return True
61
62 if graph.queryd("""ASK {
63 [ acl:agent foaf:Agent ;
64 acl:mode acl:Read ;
65 acl:accessTo ?topic ] .
66 ?photo foaf:depicts ?topic .
67 }""", initBindings={'photo' : uri}):
68 log.debug("ok, graph has an authorization for foaf:Agent ('the public') to see a topic depicted by the photo")
69 return True
70
71 if agent and graph.queryd("""
72 SELECT ?cls WHERE {
73 {
74 ?uri pho:viewableBy ?cls .
75 } UNION {
76 ?auth acl:accessTo ?uri ;
77 acl:agent ?cls ;
78 acl:mode acl:Read .
79 }
80 ?agent a ?cls .
81 }
82 """, initBindings={'uri' : uri, 'agent' : agent}):
83 log.debug("ok because the user is in a class who may view the photo")
84 return True
85
86 if agent and graph.queryd("""
87 SELECT ?auth WHERE {
88 ?auth acl:agent ?agent ; acl:mode acl:Read ; acl:accessTo ?uri .
89 }
90 """, initBindings={'uri' : uri, 'agent' : agent}):
91 log.debug("ok because the user has been authorized to read the photo")
92 return True
93
94
95 if agent and graph.queryd("""
96 SELECT ?auth WHERE {
97 { ?auth acl:agent ?agentClass . } UNION {
98 ?auth acl:agentClass ?agentClass .
99 }
100 ?auth acl:mode acl:Read ; acl:accessTo ?uri .
101 ?agent a ?agentClass .
102 }
103 """, initBindings={'uri' : uri, 'agent' : agent}):
104 log.debug("ok because the user has been authorized to read the photo")
105 return True
106
107 if agent and graph.queryd("""
108 SELECT ?cls WHERE {
109 ?uri foaf:depicts ?topic .
110 ?topic pho:viewableByClass ?cls .
111 ?agent a ?cls .
112 }
113 """, initBindings={'uri' : uri, 'agent' : agent}):
114 log.debug("ok because the user is in a class who may view the photo's topic")
115 return True
116
117
118 if agent:
119 imgSet = agentImageSetCheck(graph, agent, uri)
120 if imgSet is not None:
121 log.debug("ok because user has permission to see %r", imgSet)
122 return True
123
124 # this capability doesn't appear in the make-public button
125 topicViewableBy = set(r['vb'] for r in graph.queryd("""
126 SELECT ?vb WHERE {
127 ?uri foaf:depicts ?d .
128 ?d pho:viewableBy ?vb .
129 }
130 """, initBindings={"uri" : uri}))
131 if (PHO['friends'] in topicViewableBy or
132 PHO['anyone'] in topicViewableBy):
133 log.debug("a photo topic is viewable by anyone")
134 return True
135
136 return False
137
138 @print_timing
139 def maySee(graph, agent):
140 """resources this agent has access to"""
141 return [row['access'] for row in graph.queryd("""
142 SELECT DISTINCT ?access WHERE {
143 ?auth acl:mode acl:Read ; acl:accessTo ?access .
144 {
145 ?auth acl:agent ?agent .
146 } UNION {
147 { ?auth acl:agentClass ?agentClass . } UNION { ?auth acl:agent ?agentClass . }
148 ?agent a ?agentClass .
149 }
150 }
151 """, initBindings={'agent' : agent})]
152
153 @print_timing
154 def agentImageSetCheck(graph, agent, photo):
155 """
156 does this agent (or one of its classes) have permission to
157 view some imageset that (currently) includes the image?
158
159 returns an imageset URI or None
160 """
161
162 for res in maySee(graph, agent):
163 if 'bigasterisk.com/openidProxySite' in res:
164 # some other statements about permissions got in the
165 # graph; it's wasting time to check them as images
166 continue
167 log.debug("%r can see %r - is the pic in that set?", agent, res)
168 try:
169 imgSet = ImageSetDesc(graph, agent, res)
170 except ValueError:
171 # includes permissions for things that aren't photos at all
172 continue
173 if imgSet.includesPhoto(photo):
174 return maySee
175 return None
176
177 def viewableViaInference(graph, uri, agent):
178 """
179 viewable for some reason that can't be removed here
180 """
181
182 # somehow allow local clients who could get to the filesystem
183 # anyway. maybe with a cookie file in the fs, or just ip
184 # screening
185
186 if graph.queryd("""
187 SELECT ?post WHERE {
188 <http://bigasterisk.com/ari/> sioc:container_of ?post .
189 ?post sioc:links_to ?uri .
190 }""", initBindings={'uri' : uri}): # should just be ASK
191 # this should also be limiting to readers of the blog!
192 log.debug("ok because it's on the blog")
193 return True
194
195 return False
196
197 def viewableBySuperAgent(agent):
198 # not final; just matching the old logic. Oops- this fast one gets
199 # checked after some really slow queries. But that's ok; I get a
200 # better taste of the slowness of the site this way
201 if agent in auth.superagents:
202 log.debug("ok, agent %r is in superagents", agent)
203 return True
204 return False
205
206
207 @print_timing
208 def viewable(graph, uri, agent):
209 """
210 should the resource at uri be retrievable by this agent
211 """
212 log.debug("viewable check for %s to see %s", agent, uri)
213 ok = (viewableBySuperAgent(agent) or
214 viewableViaPerm(graph, uri, agent) or
215 viewableViaInference(graph, uri, agent))
216 if not ok:
217 log.debug("not viewable")
218
219 return ok
220
221
222 def interestingAclAgentsAndClasses(graph):
223 """
224 what agents and classes should we offer to set perms by?
225 returns rows with ?uri and ?label
226 """
227 return graph.queryd("""
228 SELECT DISTINCT ?uri ?label WHERE {
229 ?uri a pho:AgentsForAclUi .
230 OPTIONAL { ?uri rdfs:label ?label }
231 }""")
232
233 def agentMaySetAccessControl(agent):
234 # todo: read acl.Control settings
235 return agent in auth.superagents
236
237 # belongs in another module
238 def expandPhotos(graph, user, subject):
239 """
240 returns the set of images that this subject refers to, plus a
241 short description of the set
242
243 subject can be a photo itself, some search query, a topic, etc
244 """
245
246 # this may be redundant with what ImageSetDesc does with one photo?
247 if graph.contains((subject, RDF.type, FOAF.Image)):
248 return [subject], "this photo"
249
250 #URIRef('http://photo.bigasterisk.com/set?current=http%3A%2F%2Fphoto.bigasterisk.com%2Femail%2F2010-11-15%2FP1010194.JPG&dir=http%3A%2F%2Fphoto.bigasterisk.com%2Femail%2F2010-11-15%2F')
251 pics = ImageSetDesc(graph, user, subject).photos()
252 if len(pics) == 1:
253 return pics, "this photo"
254 else:
255 return pics, "these %s photos" % len(pics)
256
257 def agentCanSeeAllPhotos(graph, agent, photos):
258 """
259 may return 'inferred' to mean 'yes, but at least one is not from a
260 permission setting that can be removed'
261
262 this is a problem because if just one in the set is inferred, you
263 should be able to set a perm that covers the rest but remove that
264 perm later. The UI should probably present inferred access totally
265 separately
266 """
267 someInferred = False
268 for p in photos:
269 viaPerm = viewableViaPerm(graph, p, agent)
270 viaInfer = viewableViaInference(graph, p, agent)
271 if not viaPerm and not viaInfer:
272 return False
273 if not viaPerm:
274 someInferred = True
275 log.debug("%s ok for %s" % (p, agent))
276
277 return 'inferred' if someInferred else True
278
279 def accessControlWidget(graph, agent, subject):
280 """
281 subject might be one picture or some collection
282
283 some agents or classes might have access due to some other facts,
284 so you can't turn them off from this UI.
285
286 result is a dict for interpolating to aclwidget.html
287 """
288 if not agentMaySetAccessControl(agent):
289 return ""
290
291 photos, groupDesc = expandPhotos(graph, agent, subject)
292
293 def agentRowSetting(agent, subject):
294 if authorizations(graph, row['uri'], subject):
295 return True
296 if all(agentClassCheck(graph, row['uri'], p) for p in photos):
297 return 'inferred'
298 return False
299
300 agents = []
301 for row in interestingAclAgentsAndClasses(graph):
302 setting = agentRowSetting(row['uri'], subject)
303 agents.append(dict(
304 label=row['label'],
305 setting=setting,
306 uri=row['uri'],
307 liClass='inferred' if setting=='inferred' else '',
308 checked='checked="checked"' if setting else '',
309 disabled='disabled="disabled"' if setting == 'inferred' else '',
310 checkId="id-%s" % (random.randint(0,9999999)),
311 authorizations=describeAuthorizations(graph, row['uri'], subject),
312 ))
313
314 # i think the correct thing to list would be *previous perm
315 # settings that enabled access to these photos* so it's clear what
316 # you're removing even when it's not an exact match to the photo
317 # set. If the perm setting corresponds exactly to a perm you could
318 # give (e.g. it was one you just applied), then use the normal
319 # checkbox list. For anything else, split them out and make the
320 # action be 'remove perm'
321 ret = dict(
322 desc=groupDesc,
323 about=subject,
324 agents=agents,
325 )
326 return ret
327
328 def describeAuthorizations(graph, forAgent, accessTo):
329 ret = []
330 for auth in authorizations(graph, forAgent, accessTo):
331 rows = graph.queryd("""SELECT DISTINCT ?creator ?created WHERE {
332 OPTIONAL { ?auth dcterms:creator ?creator }
333 OPTIONAL { ?auth dcterms:created ?created }
334 }""", initBindings={'auth' : auth})
335 if not rows:
336 rows = [{}]
337 ret.append(dict(auth=auth,
338 creator=rows[0].get('creator'),
339 created=rows[0].get('created')))
340 return ret
341
342 def addAccess(graph, user, agent, accessTo, otherStmts=[]):
343 if not agentMaySetAccessControl(user):
344 raise ValueError("user is not allowed to set access controls")
345
346 # i'm trying to resist setting access for dates, since they're not
347 # a good grouping in the first place and also because seeing one
348 # date lets you see a span of them, currently
349 assert isinstance(accessTo, URIRef)
350
351 auth = URIRef("http://photo.bigasterisk.com/access/%f" % time.time())
352 stmts = [(auth, RDF.type, ACL.Authorization),
353 (auth, ACL.mode, ACL.Read),
354
355 # the spec wants me to use accessToClass and agentClass when
356 # those things are classes, but that seems annoying
357 (auth, ACL.accessTo, accessTo),
358 (auth, ACL.agent, agent),
359
360 (auth, DCTERMS.creator, user),
361 (auth, DCTERMS.created, Literal(datetime.datetime.now(tzlocal())))]
362 stmts.extend(otherStmts)
363 subgraph = URIRef('http://photo.bigasterisk.com/update/%f' % time.time())
364 graph.add(stmts, context=subgraph)
365 log.info("wrote new auth %s to subgraph %s" % (auth, subgraph))
366
367 def cookie(n):
368 return ''.join(random.choice('bcdfghjklmnpqrstvwxz0123456789')
369 for loop in range(n))
370
371 def addByEmail(graph, user, agentEmail, accessTo):
372 """
373 send mail to agentEmail with an invite to view accessTo and a link
374 that will later log the agent in. This looks for an existing agent
375 with that address or else makes up a new one.
376
377 returns the agent that got invited
378 """
379 mbox = URIRef("mailto:%s" % agentEmail)
380 stmts = []
381 try:
382 agent = agentWithEmail(graph, mbox)
383 except ValueError:
384 agent = URIRef("http://bigasterisk.com/foaf/auto/%s" % cookie(8))
385 loginUri = URIRef(url.URL.fromString(accessTo).add("login", cookie(32)))
386 stmts.extend([
387 (agent, FOAF.mbox, mbox),
388 (agent, RDF.type, FOAF.Person),
389 (agent, DCTERMS.created, Literal(datetime.datetime.now(tzlocal()))),
390 (agent, PHO.invitedBy, user),
391 (loginUri, PHO.autoLogin, agent),
392 (loginUri, PHO.loginLinkFor, accessTo),
393 ])
394
395 addAccess(graph, user, agent, accessTo, otherStmts=stmts)
396
397 return agent
398
399
400 def agentWithEmail(graph, mbox):
401 """
402 uri of an agent with this mailto uri or else ValueError
403 """
404 rows = graph.queryd("SELECT ?agent WHERE { ?agent foaf:mbox ?mbox }",
405 initBindings={"mbox" : mbox})
406 if rows:
407 return rows[0]['agent']
408 raise ValueError("no agent with mbox %r" % mbox)
409
410 def legacyRemove(graph, agent, accessTo):
411 if graph.queryd("ASK { ?photo pho:viewableBy ?agent . }",
412 initBindings={'photo' : accessTo, 'agent' : agent}):
413 stmt = (accessTo, PHO.viewableBy, agent)
414 log.info("removing %s", stmt)
415 graph.remove([stmt])
416 return True
417 return False
418
419 def authorizations(graph, forAgent, accessTo):
420 return [row['auth'] for row in graph.queryd("""
421 SELECT ?auth WHERE {
422 ?auth acl:mode acl:Read ;
423 acl:accessTo ?photo ;
424 acl:agent ?agent .
425 }""", initBindings={'photo' : accessTo, 'agent' : forAgent})]
426
427 def removeAccess(graph, user, agent, accessTo):
428 if not agentMaySetAccessControl(user):
429 raise ValueError("user is not allowed to set access controls")
430
431 if legacyRemove(graph, agent, accessTo):
432 return
433
434 auths = authorizations(graph, agent, accessTo)
435
436 for auth in auths:
437 stmt = (auth, None, None)
438 log.info("removing %s", stmt)
439 graph.remove([stmt])
440
441 if not auths:
442 raise ValueError("didn't find any access to %s for %s" %
443 (accessTo, agent))
444
445
446 # older
447 def isPublic(graph, uri):
448 return viewable(graph, uri, FOAF.Agent)
449
450 def makePublic(uri):
451 return makePublics([uri])
452
453 def makePublics(uris):
454 writeStatements([
455 (uri, PHO.viewableBy, PHO.friends) for uri in uris
456 ])
457