SandBox: axiomguarded.tac

File axiomguarded.tac, 22.4 kB (added by rwall, 3 years ago)

An example of axiom userbase and nevow guard with some other bits.

Line 
1 from datetime import datetime
2
3 from zope.interface import implements, Interface
4
5 from twisted.application import service, strports
6 from twisted.cred import portal, checkers, credentials
7 from twisted.cred.credentials import UsernamePassword
8 from twisted.python.components import registerAdapter
9
10 from nevow import appserver, guard, inevow, loaders, rend, stan, tags as T, url
11 from formless import annotate, webform
12
13 from axiom import attributes, item, store, substore, userbase
14
15 LOGIN_URL_SEGMENT = "login"
16
17 ##############################
18 class NewsItem(item.Item):
19     title = attributes.text(allowNone=False)
20     story = attributes.text(allowNone=False)
21     authorName = attributes.text(allowNone=False)
22     dateLastUpdated = attributes.bytes()
23    
24 class INewsManager(Interface):
25     def list(offset=None, limit=None):
26         pass
27    
28     def save(title, story, storeId):
29         pass
30    
31     def delete(storeId):
32         pass
33    
34 class NewsManager(object):
35     implements(INewsManager)
36     def __init__(self, avatar, store):
37         self.avatar = avatar
38         self.store = store
39    
40     def list(self, offset=None, limit=None):
41         return self.store.query(NewsItem)
42    
43     def save(self, title, story, storeId=None):
44         if storeId:
45             item = self.store.getItemById(storeId)
46             item.title = title
47             item.story = story
48         else:
49             item = NewsItem(
50                 store=self.store,
51                 title=title,
52                 story=story,
53                 authorName=IPerson(self.avatar).fullName())
54        
55         item.dateLastUpdated = datetime.now().strftime("%Y-%m-%d")
56
57 def avatarToNewsManagerAdapter(avatar):
58     if avatar.parent is None:
59         return NewsManager(avatar, avatar)
60     else:
61         return NewsManager(avatar, avatar.parent)
62
63 registerAdapter(avatarToNewsManagerAdapter, store.Store, INewsManager)
64
65 #############################
66 class NewsSaveFragment(rend.Fragment):
67     def __init__(self, *args, **kwargs):
68         self.avatar = kwargs.pop('avatar')
69         super(NewsSaveFragment, self).__init__(*args, **kwargs)
70
71     def bind_save(self, ctx):
72         return [
73             ('title', annotate.String(required=True, unicode=True)),
74             ('story', annotate.Text(required=True, unicode=True)),
75         ]
76    
77     def save(self, title, story):
78         INewsManager(self.avatar).save(title, story)
79    
80     def render_inputform(self, ctx, data):
81         return webform.renderForms("NewsSaveFragment")
82    
83 class NewsListFragment(rend.Fragment):
84     def __init__(self, *args, **kwargs):
85         self.avatar = kwargs.pop('avatar', None)
86         super(NewsListFragment, self).__init__(*args, **kwargs)
87        
88     def data_items(self, ctx, data):
89         return INewsManager(self.avatar).list()
90
91     def render_axiomitem(self, ctx, data):
92         for k, v in data.getSchema():
93             ctx.tag.fillSlots(k, getattr(data, k))
94         return ctx.tag
95
96 class LoginStatusFragment(rend.Fragment):
97     def __init__(self, *args, **kwargs):
98         self.avatar = kwargs.pop('avatar', None)
99         super(LoginStatusFragment, self).__init__(*args, **kwargs)
100    
101     def rend(self, ctx, data):
102         res = rend.Fragment.rend(self, ctx, data)
103         person = IPerson(self.avatar, None)
104         if person:
105             pattern = res.tag.onePattern("loggedin")
106             pattern.fillSlots('username', person.firstName)
107         else:
108             pattern = res.tag.onePattern("anonymous")
109
110         res.tag[pattern]
111         return res
112
113 ##############################
114 class GuardedResourceWrapper:
115     implements(inevow.IResource)
116    
117     def __init__(self, wrapped, avatar):
118         self.wrapped = wrapped
119         self.avatar = avatar
120    
121     def renderHTTP(self, ctx):
122         req = inevow.IRequest(ctx)
123         if self.avatar.avatarId is checkers.ANONYMOUS:
124             return url.root.child(LOGIN_URL_SEGMENT).add('nextUri', req.uri)
125         else:
126             return self.wrapped.renderHTTP(ctx)
127        
128     def locateChild(self, ctx, segments):
129         req = inevow.IRequest(ctx)
130         if self.avatar.avatarId is checkers.ANONYMOUS:
131             return url.root.child(LOGIN_URL_SEGMENT).add('nextUri', req.uri), []
132         else:
133             return self.wrapped, segments
134
135 class UnauthorisedFragment:
136     implements(inevow.IRenderer)
137     def __init__(self, avatar, requiredRole):
138         self.avatar = avatar
139         self.requiredRole = requiredRole
140        
141     def rend(self, ctx, data):
142         req = inevow.IRequest(ctx)
143         if self.requiredRole in IRole(self.avatar).allRoles():
144             return self.wrappedFactory(avatar=self.avatar, **self.kwargs).rend(ctx, data)
145         else:
146             return T.fieldset[
147                 T.legend["You must be logged in with role %s to use this fragment." % self.requiredRole],
148                 T.p[
149                     "[",
150                     T.a(href=url.root.child(LOGIN_URL_SEGMENT).add('nextUri', req.uri))["Login"],
151                     "]"
152                 ]
153             ]
154
155 def guardFrag(wrappedFactory, requiredRole):
156     def fragFactory(**kwargs):
157         avatar = kwargs.get('avatar')
158         if requiredRole in IRole(avatar).allRoles():
159             return wrappedFactory(**kwargs)
160         else:
161             return UnauthorisedFragment(avatar, requiredRole)
162     return fragFactory
163
164 ##############################
165 class PageWithAvatar(rend.Page):
166     def __init__(self, *args, **kwargs):
167         self.avatar = kwargs.pop('avatar')
168         super(PageWithAvatar, self).__init__(*args, **kwargs)
169
170 class IPatternFactory(Interface):
171     pass
172
173 class BasePage(PageWithAvatar):
174     addSlash = False
175     docFactory = loaders.xmlstr("""
176 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
177            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
178 <html xmlns:n="http://nevow.com/ns/nevow/0.1" n:render="pattern page"></html>
179 """)
180     patternFactory = None
181    
182     def __init__(self, *args, **kwargs):
183         super(BasePage, self).__init__(*args, **kwargs)
184         store = IRole(self.avatar).store
185        
186         EVERYONE = getEveryoneRole(store)
187         AUTHENTICATED = getAuthenticatedRole(store)
188        
189         self.allowedFragments = dict(
190             LoginStatusFragment = guardFrag(LoginStatusFragment, EVERYONE),
191             NewsListFragment = guardFrag(NewsListFragment, EVERYONE),
192             NewsSaveFragment = guardFrag(NewsSaveFragment, AUTHENTICATED),
193         )
194
195     def beforeRender(self, ctx):
196         if self.patternFactory:
197             ctx.remember(self.patternFactory, IPatternFactory)
198            
199     def locateChild(self, ctx, segments):
200         name = segments[0]
201         # XXX: Hack to allow formless to find the configurables in fragments
202         if name.startswith('freeform_post!'):
203             configurableName, bindingName = name.split('!')[1:3]
204             fragFactory = self.allowedFragments.get(configurableName, None)
205             if fragFactory is not None:
206                 frag = fragFactory(avatar=self.avatar)
207                 #print "LOCATECHILD SETTING: configurable_", frag.__class__.__name__, " TO ", frag
208                 setattr(self, "configurable_%s" % frag.__class__.__name__, lambda ctx: frag)
209                
210         if self.patternFactory:
211             ctx.remember(self.patternFactory, IPatternFactory)
212         return super(BasePage, self).locateChild(ctx, segments)
213
214     def locateFragment(self, fragmentName, **kwargs):
215         fragFactory = self.allowedFragments[fragmentName]
216         frag = fragFactory(**kwargs)
217         # So that fragments work with formless
218         #print "LOCATEFRAG SETTING: configurable_", frag.__class__.__name__, " TO ", frag
219         setattr(self, "configurable_%s" % frag.__class__.__name__, lambda ctx: frag)
220         return frag
221        
222     def render_fragment(self, name, *args):
223         def f(ctx, data):
224             kwargs = dict(
225                 avatar=self.avatar,
226                 docFactory = loaders.stan(ctx.tag.children)
227             )
228             return ctx.tag.clear()[self.locateFragment(name, **kwargs)]
229         return f
230
231     def render_pattern(self, name):
232         def f(ctx, data):
233             pattern = self.findPattern(ctx, name)
234             if pattern is not None:
235                 return ctx.tag.clear()[pattern.children]
236             return ctx.tag
237         return f
238
239     def findPattern(self, ctx, name, depth=1):
240         try:
241             doc = ctx.locate(IPatternFactory, depth)
242         except KeyError:
243             return None
244         try:
245             return inevow.IQ(doc).onePattern(name)
246         except stan.NodeNotFound:
247             return self.findPattern(ctx, name, depth+1)
248
249 class LoginPage(BasePage):
250    
251     patternFactory = loaders.xmlstr("""
252 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
253     <head>
254         <title n:pattern="page_title">Login</title>
255     </head>
256     <body>
257         <div n:pattern="page_body_content">
258             <h1>Login</h1>
259             <n:invisible n:render="loginform" />
260         </div>
261     </body>
262 </html>
263 """)
264    
265     def bind_login(self, ctx):
266         return [
267             ('ctx', annotate.Context()),
268             ('username', annotate.String(required=True)),
269             ('password', annotate.PasswordEntry(required=True)),
270         ]
271    
272     def login(self, ctx, username, password):
273         sess = inevow.ISession(ctx)
274         req = inevow.IRequest(ctx)
275         credentials = UsernamePassword(username, password)
276        
277         def errorHandler(error):
278             raise annotate.ValidateError(
279                 {},
280                 formErrorMessage="Your username and / or password were not recognised.",
281                 partialForm={'username':username})
282        
283         def successHandler(pageAndRemainingSegments):
284             return url.root
285        
286         return sess.guard.login(req, sess, credentials, []).addCallback(successHandler).addErrback(errorHandler)
287
288     def render_loginform(self, ctx, data):
289         return ctx.tag.clear()[webform.renderForms()]
290    
291 ###################################
292 class RootPage(BasePage):
293     addSlash = True
294     patternFactory = loaders.xmlstr("""
295 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
296     <head>
297         <title n:pattern="page_title">Homepage</title>
298     </head>
299     <body>
300         <div n:pattern="page_body_menu">
301             <ul>
302                 <li><a href="/">Home</a></li>
303                 <li><a href="/admin">Administration</a></li>
304                 <li><a href="/news">News</a></li>
305             </ul>
306         </div>
307         <div n:pattern="page_body_content">
308             <h1>Homepage</h1>
309             <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam vestibulum pretium augue. Maecenas a risus. Morbi quis ante sit amet orci ultrices iaculis. Nullam ut ante nec lacus elementum sagittis. Duis non magna vitae erat gravida pellentesque. In sit amet purus sed quam hendrerit luctus. Nullam ac turpis ac orci dictum sollicitudin. Fusce sem ligula, vehicula in, hendrerit a, tempus vitae, sem. Phasellus odio. In vitae tellus nec nunc lobortis venenatis.</p>
310         </div>
311     </body>
312 </html>
313 """)
314    
315     def child_form_css(self, ctx):
316         return webform.defaultCSS
317    
318     def child_login(self, ctx):
319         return LoginPage(avatar=self.avatar)
320
321     def child_news(self, ctx):
322         return NewsPage(avatar=self.avatar)
323
324     def child_admin(self, ctx):
325         return AdminPage(avatar=self.avatar)
326
327 class AdminPage(BasePage):
328     patternFactory = loaders.xmlstr("""
329 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
330     <head>
331         <title n:pattern="page_title">Administration</title>
332     </head>
333     <body>
334         <div n:pattern="page_body_menu">
335             <ul>
336                 <li><a href="/admin/news">News Admin</a></li>
337             </ul>
338         </div>
339         <div n:pattern="page_body_content">
340             <h1>Administration</h1>
341             <p>Welcome to the administration page.</p>
342         </div>
343     </body>
344 </html>
345 """)
346
347     def child_news(self, ctx):
348         return NewsAdminPage(avatar=self.avatar)
349
350 class NewsAdminPage(BasePage):
351     patternFactory = loaders.xmlstr("""
352 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
353     <head>
354         <title n:pattern="page_title">News Administration</title>
355     </head>
356     <body>
357         <div n:pattern="page_body_menu">
358             <ul>
359                 <li><a href="/admin/news">News Admin</a></li>
360             </ul>
361         </div>
362         <div n:pattern="page_body_content">
363             <h1>News Administration</h1>
364             <n:invisible  n:render="fragment NewsSaveFragment">
365             <div n:render="inputform" />
366             </n:invisible>
367
368             <div n:render="fragment NewsListFragment">
369                 <h2 class="tab">Existing News</h2>
370                 <table n:render="sequence" n:data="items" class="" style="width:100%;" cellpadding="0" cellspacing="0" border="1" id="feedback-grid">
371                     <thead n:pattern="header">
372                         <tr>
373                             <td>Title</td>
374                             <td>Author</td>
375                             <td>Date</td>
376                         </tr>
377                     </thead>
378                     <tbody>     
379                         <tr n:pattern="item">
380                         <n:invisible n:render="axiomitem">
381                             <td><n:slot name="title" /></td>
382                             <td><n:slot name="authorName" /></td>
383                             <td><n:slot name="dateLastUpdated" /></td>
384                         </n:invisible>
385                         </tr>
386                     </tbody>
387                     <tr n:pattern="empty">
388                         <td colspan="3">No items found</td>
389                     </tr>
390                 </table>
391             </div>
392         </div>
393     </body>
394 </html>
395 """)
396
397 class NewsPage(BasePage):
398     patternFactory = loaders.xmlstr("""
399 <html xmlns:n="http://nevow.com/ns/nevow/0.1">
400     <head>
401         <title n:pattern="page_title">News List</title>
402     </head>
403     <body>
404         <div n:pattern="page_body_content">
405             <h1>Latest News</h1>
406             <p>This is a list of the latest news items in our database</p>
407             <n:invisible n:render="fragment NewsListFragment">
408                 <ul n:render="sequence" n:data="items">
409                     <li n:pattern="item">
410                         <n:invisible n:render="axiomitem">
411                             <h3><n:slot name="title" /></h3>
412                             <p><n:slot name="story" /></p>
413                             <p>
414                                 By: <strong><n:slot name="authorName" /></strong>,
415                                 Last Updated: <strong><n:slot name="dateLastUpdated" /></strong>
416                             </p>
417                         </n:invisible>
418                     </li>
419                     <li n:pattern="empty">
420                         No items found
421                     </li>
422                 </ul>
423             </n:invisible>
424         </div>
425     </body>
426 </html>
427 """)
428
429 def siteRootFactory(avatar):
430     root = RootPage(avatar=avatar)
431     return root
432 registerAdapter(siteRootFactory, store.Store, inevow.IResource)
433
434 ################################
435 class IPerson(Interface):
436     pass
437
438 class Person(item.Item):
439     implements(IPerson)
440    
441     typeName = "Person"
442    
443     firstName = attributes.text(allowNone=False)
444     lastName = attributes.text(allowNone=False)
445    
446     installedOn = attributes.reference()
447    
448     def installOn(self, other):
449         assert self.installedOn is None, 'cannot install Person on more than one User'
450         self.installedOn = other
451         other.powerUp(self, IPerson)
452        
453     def fullName(self):
454         return u"%s %s" % (self.firstName, self.lastName)
455
456 ##############################
457 class IAxiomStore(Interface):
458     pass
459 registerAdapter(lambda substoreItem: substoreItem.open(), substore.SubStore, IAxiomStore)
460
461 class IRole(Interface):
462     pass
463
464 def storeToRoleAdapter(store):
465     if store.parent is None: #anonymous avatar is the main store
466         return getEveryoneRole(store)
467     else: # logged in avatar is a user substore.
468         return getSelfRole(store)
469
470 registerAdapter(storeToRoleAdapter, store.Store, IRole)
471
472 ##############################
473 def createAccount(store, username, domain, password, firstName, lastName):
474     def transaction():
475         loginSystem = store.findFirst(userbase.LoginSystem)
476         loginSystem.installOn(store)
477        
478         account = loginSystem.addAccount(username, domain, password)
479         substore = IAxiomStore(account.avatars)
480
481         person = Person(store=substore, firstName=firstName, lastName=lastName)
482         person.installOn(substore)
483         return account
484     return store.transact(transaction)
485
486 ##################################
487 import os
488
489 DB_PATH = os.tmpnam()
490 s = store.Store(DB_PATH)
491 loginSystem = s.findOrCreate(userbase.LoginSystem)
492 createAccount(s, u"joebloggs", u"localhost", u"password", u"Joe", u"Bloggs")
493 createAccount(s, u"Janedoe", u"localhost", u"password", u"Jane", u"Doe")
494
495 ###################################
496 porta = portal.Portal(realm = loginSystem)
497 porta.registerChecker(checkers.AllowAnonymousAccess(), credentials.IAnonymous)
498 porta.registerChecker(loginSystem)
499 root = guard.SessionWrapper(porta)
500
501 patternFactory = loaders.xmlstr("""
502 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
503            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
504 <html xmlns:n="http://nevow.com/ns/nevow/0.1" n:pattern="page">
505     <head>
506         <title n:render="pattern page_title">Page Title</title>
507         <style>
508             #login-status{
509                 border-bottom : 1px solid Black;
510             }
511         </style>
512         <link rel="stylesheet" href="/form_css" type="text/css" />
513     </head>
514     <body>
515         <div n:render="pattern page_header">
516             <div n:render="fragment LoginStatusFragment" id="login-status">
517             <p n:pattern="loggedin">
518                 Welcome <strong><n:slot name="username" /></strong>, [<a href="/__logout__">Logout</a>]
519             </p>
520             <p n:pattern="anonymous">
521                 You are not logged in, [<a href="/login">Login</a>]
522             </p>
523             </div>
524         </div>
525         <div n:render="pattern page_body">
526             <table width="100%" border="1">
527                 <tr>
528                     <td n:render="pattern page_body_menu" width="20%" valign="top">
529                     <ul>
530                         <li><a href="/">Home</a></li>
531                     </ul>
532                     </td>
533                     <td n:render="pattern page_body_content" width="80%" valign="top">
534                         <h1>Site Title</h1>
535                         <p>Placeholder</p>
536                     </td>
537                 </tr>
538             </table>
539         </div>
540         <div n:render="pattern page_footer">
541             <p>&copy;Default Site Footer 2006</p>
542         </div>
543     </body>
544 </html>
545 """)
546 site = appserver.NevowSite(root)
547 site.remember(patternFactory, IPatternFactory)
548 application = service.Application("examples")
549 strports.service("8080", site).setServiceParent(application)
550
551 ####################################
552 from twisted.internet import reactor
553
554 def cleanUp():
555     print "DELETING DATABASE: ", DB_PATH
556     for root, dirs, files in os.walk(DB_PATH, topdown=False):
557         for name in files:
558             os.remove(os.path.join(root, name))
559         for name in dirs:
560             os.rmdir(os.path.join(root, name))
561     os.removedirs(DB_PATH)
562 reactor.addSystemEventTrigger("after", "shutdown", cleanUp)
563
564 #############################
565 # STOLEN from http://divmod.org/trac/browser/branches/sharing-382-5/Mantissa/xmantissa/sharing.py
566 class RoleRelationship(item.Item):
567     schemaVersion = 1
568     typeName = 'sharing_relationship'
569
570     member = attributes.reference()
571     group = attributes.reference()
572
573 class Role(item.Item):
574     schemaVersion = 1
575     typeName = 'sharing_role'
576     externalID = attributes.text(allowNone=False)
577     description = attributes.text()
578
579     def becomeMemberOf(self, groupRole):
580         self.store.findOrCreate(RoleRelationship,
581                                 group=groupRole,
582                                 member=self)
583
584     def allRoles(self, memo=None):
585         if memo is None:
586             memo = set()
587         elif self in memo:
588             # this is bad, but we have successfully detected and prevented the
589             # only really bad symptom, an infinite loop.
590             return
591         memo.add(self)
592         yield self
593         for groupRole in self.store.query(Role,
594                                           attributes.AND(RoleRelationship.member == self,
595                                               RoleRelationship.group == Role.storeID)):
596             for roleRole in groupRole.allRoles(memo):
597                 yield roleRole
598
599     def __repr__(self):
600         return self.externalID.upper()
601
602 #############################
603 def getEveryoneRole(store):
604     """
605     Get a base 'Everyone' role for this store, which is the role that every user has no matter what.
606     """
607     return store.findOrCreate(Role, externalID=u'Everyone')
608
609 def getAuthenticatedRole(store):
610     """
611     Get the base 'Authenticated' role for this store, which is the role that
612     every user with a proper username has, no matter what.
613     """
614     def tx():
615         def addToEveryone(newAuthenticatedRole):
616             newAuthenticatedRole.becomeMemberOf(getEveryoneRole(store))
617             return newAuthenticatedRole
618         return store.findOrCreate(Role, addToEveryone, externalID=u'Authenticated')
619     return store.transact(tx)
620
621 def getPrimaryRole(store, primaryRoleName, createIfNotFound=False):
622     """
623     Get Role object corresponding to an identifier name.  If the role name
624     passed is the empty string, it is assumed that the user is not
625     authenticated, and the 'Everybody' role is primary.  If the role name
626     passed is non-empty, but has no corresponding role, the 'Authenticated'
627     role - which is a member of 'Everybody' - is primary.  Finally, a specific
628     role can be primary if one exists for the user's given credentials, that
629     will automatically always be a member of 'Authenticated', and by extension,
630     of 'Everybody'.
631     """
632     if not primaryRoleName:
633         return getEveryoneRole(store)
634     ff = store.findUnique(Role, Role.externalID == primaryRoleName, default=None)
635     if ff is not None:
636         return ff
637     authRole = getAuthenticatedRole(store)
638     if createIfNotFound:
639         role = Role(store=store,
640                     externalID=primaryRoleName)
641         role.becomeMemberOf(authRole)
642         return role
643     return authRole
644
645 def getSelfRole(store):
646     """
647     Retrieve the Role which corresponds to the user to whom the given store
648     belongs.
649     """
650     for (localpart, domain) in userbase.getAccountNames(store):
651         return getPrimaryRole(store, u'%s@%s' % (localpart, domain), createIfNotFound=True)
652     raise ValueError("Cannot get self-role for unnamed account.")
jethro@divmod.org