2 implementation of http://esw.w3.org/WebAccessControl
4 currently this is contaminated with my old 'viewableBy' access system
5 and some other photo-site-specific things, but hopefully those can be
9 import logging, random, time, datetime, os
10 from dateutil.tz import tzlocal
11 from rdflib import URIRef, RDF, Literal
12 from nevow import inevow, url
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')
24 class NeedsMoreAccess(ValueError):
28 return _getUser(inevow.IRequest(ctx).getHeader)
30 def getUserWebpy(environ):
32 lambda h: environ.get('HTTP_%s' % h.upper().replace('-','_')))
34 def _getUser(getHeader):
35 if os.environ.get('PHOTO_FORCE_LOGIN', ''):
36 return URIRef(os.environ['PHOTO_FORCE_LOGIN'])
37 agent = getHeader('x-foaf-agent')
42 def agentClassCheck(graph, agent, photo):
44 is there a perm that specifically lets this agent (or class) see this photo?
46 bb = {'initBindings' : {'photo' : photo, 'agent' : agent}}
48 return (graph.queryd("ASK { ?photo pho:viewableByClass ?agent . }", **bb) or
49 graph.queryd("ASK { ?photo pho:viewableBy ?agent . }", **bb) or
50 graph.queryd("ASK { [ acl:agent ?agent ; acl:mode acl:Read ; acl:accessTo ?photo ] . }", **bb))
52 def viewableViaPerm(graph, uri, agent):
54 viewable via some permission that can be set and removed
56 log.debug("viewableViaPerm uri=%s agent=%s", uri, agent)
57 if (graph.contains((uri, PHO['viewableBy'], PHO['friends']))
58 or graph.contains((uri, PHO['viewableBy'], PHO['anyone']))):
59 log.debug("ok, graph says :viewableBy :friends or :anyone")
62 if graph.queryd("ASK { [ acl:agent pho:friends ; acl:mode acl:Read ; acl:accessTo ?photo ] .}", initBindings={'photo' : uri}):
63 log.debug("ok, graph has an authorization for :friends")
66 # spec wants me to use agentClass on this, but I've already
67 # screwed that up elsewhere, so it will take new triples to make
68 # the old data compatible
69 if graph.queryd("""ASK {
70 [ acl:agent foaf:Agent ;
72 acl:accessTo ?photo ] .
73 }""", initBindings={'photo' : uri}):
74 log.debug("ok, graph has an authorization for foaf:Agent ('the public')")
77 if graph.queryd("""ASK {
78 [ acl:agent foaf:Agent ;
80 acl:accessTo ?topic ] .
81 ?photo foaf:depicts ?topic .
82 }""", initBindings={'photo' : uri}):
83 log.debug("ok, graph has an authorization for foaf:Agent ('the public') to see a topic depicted by the photo")
86 if agent and graph.queryd("""
89 ?uri pho:viewableBy ?cls .
91 ?auth acl:accessTo ?uri ;
97 """, initBindings={'uri' : uri, 'agent' : agent}):
98 log.debug("ok because the user is in a class who may view the photo")
101 if agent and graph.queryd("""
103 ?auth acl:agent ?agent ; acl:mode acl:Read ; acl:accessTo ?uri .
105 """, initBindings={'uri' : uri, 'agent' : agent}):
106 log.debug("ok because the user has been authorized to read the photo")
110 if agent and graph.queryd("""
112 { ?auth acl:agent ?agentClass . } UNION {
113 ?auth acl:agentClass ?agentClass .
115 ?auth acl:mode acl:Read ; acl:accessTo ?uri .
116 ?agent a ?agentClass .
118 """, initBindings={'uri' : uri, 'agent' : agent}):
119 log.debug("ok because the user has been authorized to read the photo")
122 if agent and graph.queryd("""
124 ?uri foaf:depicts ?topic .
125 ?topic pho:viewableByClass ?cls .
128 """, initBindings={'uri' : uri, 'agent' : agent}):
129 log.debug("ok because the user is in a class who may view the photo's topic")
134 imgSet = agentImageSetCheck(graph, agent, uri)
135 if imgSet is not None:
136 log.debug("ok because user has permission to see %r", imgSet)
139 # this capability doesn't appear in the make-public button
140 topicViewableBy = set(r['vb'] for r in graph.queryd("""
142 ?uri foaf:depicts ?d .
143 ?d pho:viewableBy ?vb .
145 """, initBindings={"uri" : uri}))
146 if (PHO['friends'] in topicViewableBy or
147 PHO['anyone'] in topicViewableBy):
148 log.debug("a photo topic is viewable by anyone")
154 def agentImageSetCheck(graph, agent, photo):
156 does this agent (or one of its classes) have permission to
157 view some imageset that (currently) includes the image?
159 returns an imageset URI or None
162 for row in graph.queryd("""
163 SELECT DISTINCT ?access WHERE {
164 ?auth acl:mode acl:Read ; acl:accessTo ?access .
166 ?auth acl:agent ?agent .
168 { ?auth acl:agentClass ?agentClass . } UNION { ?auth acl:agent ?agentClass . }
169 ?agent a ?agentClass .
172 """, initBindings={'agent' : agent}):
173 maySee = row['access']
174 if 'bigasterisk.com/openidProxySite' in maySee:
175 # some other statements about permissions got in the
176 # graph; it's wasting time to check them as images
178 log.debug("%r can see %r - is the pic in that set?", agent, maySee)
180 imgSet = ImageSetDesc(graph, agent, maySee)
182 # includes permissions for things that aren't photos at all
184 if imgSet.includesPhoto(photo):
188 def viewableViaInference(graph, uri, agent):
190 viewable for some reason that can't be removed here
193 # somehow allow local clients who could get to the filesystem
194 # anyway. maybe with a cookie file in the fs, or just ip
199 <http://bigasterisk.com/ari/> sioc:container_of ?post .
200 ?post sioc:links_to ?uri .
201 }""", initBindings={'uri' : uri}): # should just be ASK
202 # this should also be limiting to readers of the blog!
203 log.debug("ok because it's on the blog")
208 def viewableBySuperAgent(agent):
209 # not final; just matching the old logic. Oops- this fast one gets
210 # checked after some really slow queries. But that's ok; I get a
211 # better taste of the slowness of the site this way
212 if agent in auth.superagents:
213 log.debug("ok, agent %r is in superagents", agent)
219 def viewable(graph, uri, agent):
221 should the resource at uri be retrievable by this agent
223 log.debug("viewable check for %s to see %s", agent, uri)
224 ok = (viewableBySuperAgent(agent) or
225 viewableViaPerm(graph, uri, agent) or
226 viewableViaInference(graph, uri, agent))
228 log.debug("not viewable")
233 def interestingAclAgentsAndClasses(graph):
235 what agents and classes should we offer to set perms by?
236 returns rows with ?uri and ?label
238 return graph.queryd("""
239 SELECT DISTINCT ?uri ?label WHERE {
240 ?uri a pho:AgentsForAclUi .
241 OPTIONAL { ?uri rdfs:label ?label }
244 def agentMaySetAccessControl(agent):
245 # todo: read acl.Control settings
246 return agent in auth.superagents
248 # belongs in another module
249 def expandPhotos(graph, user, subject):
251 returns the set of images that this subject refers to, plus a
252 short description of the set
254 subject can be a photo itself, some search query, a topic, etc
257 # this may be redundant with what ImageSetDesc does with one photo?
258 if graph.contains((subject, RDF.type, FOAF.Image)):
259 return [subject], "this photo"
261 #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')
262 pics = ImageSetDesc(graph, user, subject).photos()
264 return pics, "this photo"
266 return pics, "these %s photos" % len(pics)
268 def agentCanSeeAllPhotos(graph, agent, photos):
270 may return 'inferred' to mean 'yes, but at least one is not from a
271 permission setting that can be removed'
273 this is a problem because if just one in the set is inferred, you
274 should be able to set a perm that covers the rest but remove that
275 perm later. The UI should probably present inferred access totally
280 viaPerm = viewableViaPerm(graph, p, agent)
281 viaInfer = viewableViaInference(graph, p, agent)
282 if not viaPerm and not viaInfer:
286 log.debug("%s ok for %s" % (p, agent))
288 return 'inferred' if someInferred else True
290 def accessControlWidget(graph, agent, subject):
292 subject might be one picture or some collection
294 some agents or classes might have access due to some other facts,
295 so you can't turn them off from this UI.
297 result is a dict for interpolating to aclwidget.html
299 if not agentMaySetAccessControl(agent):
302 photos, groupDesc = expandPhotos(graph, agent, subject)
304 def agentRowSetting(agent, subject):
305 if authorizations(graph, row['uri'], subject):
307 if all(agentClassCheck(graph, row['uri'], p) for p in photos):
312 for row in interestingAclAgentsAndClasses(graph):
313 setting = agentRowSetting(row['uri'], subject)
318 liClass='inferred' if setting=='inferred' else '',
319 checked='checked="checked"' if setting else '',
320 disabled='disabled="disabled"' if setting == 'inferred' else '',
321 checkId="id-%s" % (random.randint(0,9999999)),
322 authorizations=describeAuthorizations(graph, row['uri'], subject),
325 # i think the correct thing to list would be *previous perm
326 # settings that enabled access to these photos* so it's clear what
327 # you're removing even when it's not an exact match to the photo
328 # set. If the perm setting corresponds exactly to a perm you could
329 # give (e.g. it was one you just applied), then use the normal
330 # checkbox list. For anything else, split them out and make the
331 # action be 'remove perm'
339 def describeAuthorizations(graph, forAgent, accessTo):
341 for auth in authorizations(graph, forAgent, accessTo):
342 rows = graph.queryd("""SELECT DISTINCT ?creator ?created WHERE {
343 OPTIONAL { ?auth dcterms:creator ?creator }
344 OPTIONAL { ?auth dcterms:created ?created }
345 }""", initBindings={'auth' : auth})
348 ret.append(dict(auth=auth,
349 creator=rows[0].get('creator'),
350 created=rows[0].get('created')))
353 def addAccess(graph, user, agent, accessTo, otherStmts=[]):
354 if not agentMaySetAccessControl(user):
355 raise ValueError("user is not allowed to set access controls")
357 # i'm trying to resist setting access for dates, since they're not
358 # a good grouping in the first place and also because seeing one
359 # date lets you see a span of them, currently
360 assert isinstance(accessTo, URIRef)
362 auth = URIRef("http://photo.bigasterisk.com/access/%f" % time.time())
363 stmts = [(auth, RDF.type, ACL.Authorization),
364 (auth, ACL.mode, ACL.Read),
366 # the spec wants me to use accessToClass and agentClass when
367 # those things are classes, but that seems annoying
368 (auth, ACL.accessTo, accessTo),
369 (auth, ACL.agent, agent),
371 (auth, DCTERMS.creator, user),
372 (auth, DCTERMS.created, Literal(datetime.datetime.now(tzlocal())))]
373 stmts.extend(otherStmts)
374 subgraph = URIRef('http://photo.bigasterisk.com/update/%f' % time.time())
375 graph.add(stmts, context=subgraph)
376 log.info("wrote new auth %s to subgraph %s" % (auth, subgraph))
379 return ''.join(random.choice('bcdfghjklmnpqrstvwxz0123456789')
380 for loop in range(n))
382 def addByEmail(graph, user, agentEmail, accessTo):
384 send mail to agentEmail with an invite to view accessTo and a link
385 that will later log the agent in. This looks for an existing agent
386 with that address or else makes up a new one.
388 returns the agent that got invited
390 mbox = URIRef("mailto:%s" % agentEmail)
393 agent = agentWithEmail(graph, mbox)
395 agent = URIRef("http://bigasterisk.com/foaf/auto/%s" % cookie(8))
396 loginUri = URIRef(url.URL.fromString(accessTo).add("login", cookie(32)))
398 (agent, FOAF.mbox, mbox),
399 (agent, RDF.type, FOAF.Person),
400 (agent, DCTERMS.created, Literal(datetime.datetime.now(tzlocal()))),
401 (agent, PHO.invitedBy, user),
402 (loginUri, PHO.autoLogin, agent),
403 (loginUri, PHO.loginLinkFor, accessTo),
406 addAccess(graph, user, agent, accessTo, otherStmts=stmts)
411 def agentWithEmail(graph, mbox):
413 uri of an agent with this mailto uri or else ValueError
415 rows = graph.queryd("SELECT ?agent WHERE { ?agent foaf:mbox ?mbox }",
416 initBindings={"mbox" : mbox})
418 return rows[0]['agent']
419 raise ValueError("no agent with mbox %r" % mbox)
421 def legacyRemove(graph, agent, accessTo):
422 if graph.queryd("ASK { ?photo pho:viewableBy ?agent . }",
423 initBindings={'photo' : accessTo, 'agent' : agent}):
424 stmt = (accessTo, PHO.viewableBy, agent)
425 log.info("removing %s", stmt)
430 def authorizations(graph, forAgent, accessTo):
431 return [row['auth'] for row in graph.queryd("""
433 ?auth acl:mode acl:Read ;
434 acl:accessTo ?photo ;
436 }""", initBindings={'photo' : accessTo, 'agent' : forAgent})]
438 def removeAccess(graph, user, agent, accessTo):
439 if not agentMaySetAccessControl(user):
440 raise ValueError("user is not allowed to set access controls")
442 if legacyRemove(graph, agent, accessTo):
445 auths = authorizations(graph, agent, accessTo)
448 stmt = (auth, None, None)
449 log.info("removing %s", stmt)
453 raise ValueError("didn't find any access to %s for %s" %
458 def isPublic(graph, uri):
459 return viewable(graph, uri, FOAF.Agent)
462 return makePublics([uri])
464 def makePublics(uris):
466 (uri, PHO.viewableBy, PHO.friends) for uri in uris