/
/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, os
10 from dateutil.tz import tzlocal
11 from rdflib import URIRef, RDF, Literal
12 from nevow import inevow, 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 getUser(ctx):
28 return _getUser(inevow.IRequest(ctx).getHeader)
29
30 def getUserWebpy(environ):
31 return _getUser(
32 lambda h: environ.get('HTTP_%s' % h.upper().replace('-','_')))
33
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')
38 if agent is None:
39 return None
40 return URIRef(agent)
41
42 def agentClassCheck(graph, agent, photo):
43 """
44 is there a perm that specifically lets this agent (or class) see this photo?
45 """
46 bb = {'initBindings' : {'photo' : photo, 'agent' : agent}}
47 # UNION was breaking
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))
51
52 def viewableViaPerm(graph, uri, agent):
53 """
54 viewable via some permission that can be set and removed
55 """
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")
60 return True
61
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")
64 return True
65
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 ;
71 acl:mode acl:Read ;
72 acl:accessTo ?photo ] .
73 }""", initBindings={'photo' : uri}):
74 log.debug("ok, graph has an authorization for foaf:Agent ('the public')")
75 return True
76
77 if graph.queryd("""ASK {
78 [ acl:agent foaf:Agent ;
79 acl:mode acl:Read ;
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")
84 return True
85
86 if agent and graph.queryd("""
87 SELECT ?cls WHERE {
88 {
89 ?uri pho:viewableBy ?cls .
90 } UNION {
91 ?auth acl:accessTo ?uri ;
92 acl:agent ?cls ;
93 acl:mode acl:Read .
94 }
95 ?agent a ?cls .
96 }
97 """, initBindings={'uri' : uri, 'agent' : agent}):
98 log.debug("ok because the user is in a class who may view the photo")
99 return True
100
101 if agent and graph.queryd("""
102 SELECT ?auth WHERE {
103 ?auth acl:agent ?agent ; acl:mode acl:Read ; acl:accessTo ?uri .
104 }
105 """, initBindings={'uri' : uri, 'agent' : agent}):
106 log.debug("ok because the user has been authorized to read the photo")
107 return True
108
109
110 if agent and graph.queryd("""
111 SELECT ?auth WHERE {
112 { ?auth acl:agent ?agentClass . } UNION {
113 ?auth acl:agentClass ?agentClass .
114 }
115 ?auth acl:mode acl:Read ; acl:accessTo ?uri .
116 ?agent a ?agentClass .
117 }
118 """, initBindings={'uri' : uri, 'agent' : agent}):
119 log.debug("ok because the user has been authorized to read the photo")
120 return True
121
122 if agent and graph.queryd("""
123 SELECT ?cls WHERE {
124 ?uri foaf:depicts ?topic .
125 ?topic pho:viewableByClass ?cls .
126 ?agent a ?cls .
127 }
128 """, initBindings={'uri' : uri, 'agent' : agent}):
129 log.debug("ok because the user is in a class who may view the photo's topic")
130 return True
131
132
133 if agent:
134 imgSet = agentImageSetCheck(graph, agent, uri)
135 if imgSet is not None:
136 log.debug("ok because user has permission to see %r", imgSet)
137 return True
138
139 # this capability doesn't appear in the make-public button
140 topicViewableBy = set(r['vb'] for r in graph.queryd("""
141 SELECT ?vb WHERE {
142 ?uri foaf:depicts ?d .
143 ?d pho:viewableBy ?vb .
144 }
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")
149 return True
150
151 return False
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 row in graph.queryd("""
163 SELECT DISTINCT ?access WHERE {
164 ?auth acl:mode acl:Read ; acl:accessTo ?access .
165 {
166 ?auth acl:agent ?agent .
167 } UNION {
168 { ?auth acl:agentClass ?agentClass . } UNION { ?auth acl:agent ?agentClass . }
169 ?agent a ?agentClass .
170 }
171 }
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
177 continue
178 log.debug("%r can see %r - is the pic in that set?", agent, maySee)
179 try:
180 imgSet = ImageSetDesc(graph, agent, maySee)
181 except ValueError:
182 # includes permissions for things that aren't photos at all
183 continue
184 if imgSet.includesPhoto(photo):
185 return maySee
186 return None
187
188 def viewableViaInference(graph, uri, agent):
189 """
190 viewable for some reason that can't be removed here
191 """
192
193 # somehow allow local clients who could get to the filesystem
194 # anyway. maybe with a cookie file in the fs, or just ip
195 # screening
196
197 if graph.queryd("""
198 SELECT ?post WHERE {
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")
204 return True
205
206 return False
207
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)
214 return True
215 return False
216
217
218 @print_timing
219 def viewable(graph, uri, agent):
220 """
221 should the resource at uri be retrievable by this agent
222 """
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))
227 if not ok:
228 log.debug("not viewable")
229
230 return ok
231
232
233 def interestingAclAgentsAndClasses(graph):
234 """
235 what agents and classes should we offer to set perms by?
236 returns rows with ?uri and ?label
237 """
238 return graph.queryd("""
239 SELECT DISTINCT ?uri ?label WHERE {
240 ?uri a pho:AgentsForAclUi .
241 OPTIONAL { ?uri rdfs:label ?label }
242 }""")
243
244 def agentMaySetAccessControl(agent):
245 # todo: read acl.Control settings
246 return agent in auth.superagents
247
248 # belongs in another module
249 def expandPhotos(graph, user, subject):
250 """
251 returns the set of images that this subject refers to, plus a
252 short description of the set
253
254 subject can be a photo itself, some search query, a topic, etc
255 """
256
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"
260
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()
263 if len(pics) == 1:
264 return pics, "this photo"
265 else:
266 return pics, "these %s photos" % len(pics)
267
268 def agentCanSeeAllPhotos(graph, agent, photos):
269 """
270 may return 'inferred' to mean 'yes, but at least one is not from a
271 permission setting that can be removed'
272
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
276 separately
277 """
278 someInferred = False
279 for p in photos:
280 viaPerm = viewableViaPerm(graph, p, agent)
281 viaInfer = viewableViaInference(graph, p, agent)
282 if not viaPerm and not viaInfer:
283 return False
284 if not viaPerm:
285 someInferred = True
286 log.debug("%s ok for %s" % (p, agent))
287
288 return 'inferred' if someInferred else True
289
290 def accessControlWidget(graph, agent, subject):
291 """
292 subject might be one picture or some collection
293
294 some agents or classes might have access due to some other facts,
295 so you can't turn them off from this UI.
296
297 result is a dict for interpolating to aclwidget.html
298 """
299 if not agentMaySetAccessControl(agent):
300 return ""
301
302 photos, groupDesc = expandPhotos(graph, agent, subject)
303
304 def agentRowSetting(agent, subject):
305 if authorizations(graph, row['uri'], subject):
306 return True
307 if all(agentClassCheck(graph, row['uri'], p) for p in photos):
308 return 'inferred'
309 return False
310
311 agents = []
312 for row in interestingAclAgentsAndClasses(graph):
313 setting = agentRowSetting(row['uri'], subject)
314 agents.append(dict(
315 label=row['label'],
316 setting=setting,
317 uri=row['uri'],
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),
323 ))
324
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'
332 ret = dict(
333 desc=groupDesc,
334 about=subject,
335 agents=agents,
336 )
337 return ret
338
339 def describeAuthorizations(graph, forAgent, accessTo):
340 ret = []
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})
346 if not rows:
347 rows = [{}]
348 ret.append(dict(auth=auth,
349 creator=rows[0].get('creator'),
350 created=rows[0].get('created')))
351 return ret
352
353 def addAccess(graph, user, agent, accessTo, otherStmts=[]):
354 if not agentMaySetAccessControl(user):
355 raise ValueError("user is not allowed to set access controls")
356
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)
361
362 auth = URIRef("http://photo.bigasterisk.com/access/%f" % time.time())
363 stmts = [(auth, RDF.type, ACL.Authorization),
364 (auth, ACL.mode, ACL.Read),
365
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),
370
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))
377
378 def cookie(n):
379 return ''.join(random.choice('bcdfghjklmnpqrstvwxz0123456789')
380 for loop in range(n))
381
382 def addByEmail(graph, user, agentEmail, accessTo):
383 """
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.
387
388 returns the agent that got invited
389 """
390 mbox = URIRef("mailto:%s" % agentEmail)
391 stmts = []
392 try:
393 agent = agentWithEmail(graph, mbox)
394 except ValueError:
395 agent = URIRef("http://bigasterisk.com/foaf/auto/%s" % cookie(8))
396 loginUri = URIRef(url.URL.fromString(accessTo).add("login", cookie(32)))
397 stmts.extend([
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),
404 ])
405
406 addAccess(graph, user, agent, accessTo, otherStmts=stmts)
407
408 return agent
409
410
411 def agentWithEmail(graph, mbox):
412 """
413 uri of an agent with this mailto uri or else ValueError
414 """
415 rows = graph.queryd("SELECT ?agent WHERE { ?agent foaf:mbox ?mbox }",
416 initBindings={"mbox" : mbox})
417 if rows:
418 return rows[0]['agent']
419 raise ValueError("no agent with mbox %r" % mbox)
420
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)
426 graph.remove([stmt])
427 return True
428 return False
429
430 def authorizations(graph, forAgent, accessTo):
431 return [row['auth'] for row in graph.queryd("""
432 SELECT ?auth WHERE {
433 ?auth acl:mode acl:Read ;
434 acl:accessTo ?photo ;
435 acl:agent ?agent .
436 }""", initBindings={'photo' : accessTo, 'agent' : forAgent})]
437
438 def removeAccess(graph, user, agent, accessTo):
439 if not agentMaySetAccessControl(user):
440 raise ValueError("user is not allowed to set access controls")
441
442 if legacyRemove(graph, agent, accessTo):
443 return
444
445 auths = authorizations(graph, agent, accessTo)
446
447 for auth in auths:
448 stmt = (auth, None, None)
449 log.info("removing %s", stmt)
450 graph.remove([stmt])
451
452 if not auths:
453 raise ValueError("didn't find any access to %s for %s" %
454 (accessTo, agent))
455
456
457 # older
458 def isPublic(graph, uri):
459 return viewable(graph, uri, FOAF.Agent)
460
461 def makePublic(uri):
462 return makePublics([uri])
463
464 def makePublics(uris):
465 writeStatements([
466 (uri, PHO.viewableBy, PHO.friends) for uri in uris
467 ])
468