1import string
2import urllib
3
4from zope.interface import implements
5
6from twisted.internet import defer
7from twisted.python import failure
8from twisted.application import strports
9from twisted.application import service
10from twisted.python import components
11from twisted.web import util
12from twisted.cred import portal
13from twisted.cred import checkers
14from twisted.cred import credentials
15from twisted.cred import error
16
17from nevow import inevow
18from nevow import loaders
19from nevow import rend
20from nevow import appserver
21from nevow import static
22from nevow import url
23from nevow import guard
24from nevow.tags import *
25
26from formless import annotate
27from formless import iformless
28from formless import webform
29
30#=============================================================================
31# Domain Objects
32#=============================================================================
33class User(object):
34    """ultra-basic domain object"""
35    def __init__(self, username, password, email, role="USER"):
36        self.username = username
37        self.password = password
38        self.email = email
39        self.role = role
40
41    def __repr__(self):
42        return "UserObject id: %d, %s"%(id(self), self.__dict__)
43
44class UserDictDB(object):
45    """In memory data-manager for User objects"""
46    def __init__(self):
47        self.users = {}
48
49    def createUser(self, username, password, email, role=None):
50        #XXX--should check for duplicates
51        if not role:
52            role = "USER"
53        user = User(username, password, email, role)
54        self.users[username] = user
55        return user
56
57    def deleteUser(self, username):
58        del self.users[username]
59
60    def findAllUsers(self):
61        return self.users.values()
62
63    def findUser(self, username):
64        return self.users.get(username)
65
66#=============================================================================
67# Formless Interfaces
68#=============================================================================
69class ICreateUser(annotate.TypedInterface):
70    """createUser is an autocallable method to create a new user in the userDB"""
71    def createUser(
72        self,
73        request=annotate.Request(),
74        username=annotate.String(
75            label="Username: ",
76            description="Unique system name for this user.",
77            required=True),
78        password=annotate.Password(
79            label="Password: ",
80            description="Password for this user. Used in authentication.",
81            required=True),
82        email=annotate.String(
83            label="Email: ",
84            description="Primary email address for this user."),
85        role=annotate.Choice(
86            label="Role: ",
87            #description="Role for this user. Choices are: USER, ADMIN",
88            #show how you can inline stan tags in the description--cool!
89            description=span["Role for this user. Choices are: ",
90                             strong["USER, ADMIN"]],
91            choices=["USER","ADMIN"],
92            default="USER")):
93        """Create User:
94
95        Create a new User
96        """
97        pass
98
99    createUser = annotate.autocallable(createUser, action="Create")
100
101class IEditUser(annotate.TypedInterface):
102    """used in edit screens. we can only edit the email address,
103    not the username"""
104    email = annotate.String(
105        label="Email: ",
106        description="Primary email address for this user.")
107    password = annotate.Password(
108        label="Password: ",
109        description="Password for this user. Used in authentication.")
110
111
112class IDeleteUser(annotate.TypedInterface):
113    """deleteUser is an autocallable method to delete a user in the userDB"""
114    def deleteUser(self, request=annotate.Request()):
115        """Delete User:
116
117        Delete this User.
118        """
119        pass
120
121    deleteUser = annotate.autocallable(deleteUser, action="Delete")
122
123class ILogout(annotate.TypedInterface):
124    """Call this to logout and destroy the current session"""
125    def logout(self, request=annotate.Request()):
126        """Logout:
127
128        Sorry to see you go. Please visit again.
129        """
130        pass
131
132    logout = annotate.autocallable(logout, action="logout")
133
134#=============================================================================
135# Pages
136#=============================================================================
137class ICurrentUser(components.Interface):
138    """The current logged-in user object
139    """
140
141class IUserManager(components.Interface):
142    """The user manager object
143    """
144
145class NotLoggedIn(rend.Page):
146    """The resource that is returned when you are not logged in"""
147
148    addSlash = True
149
150    docFactory = loaders.stan(html[
151        head[title["Not Logged In"]],
152        body[
153            p[
154              """Welcome to the wonderful world of the UserDB app.""",
155              br(),
156              """Login to begin. (Hint: admin's password is admin, foo's password
157              is foo, can you guess what bar's password is? ;-)"""
158            ],
159            form(action=guard.LOGIN_AVATAR, method='post')[
160                table[
161                    tr[
162                        td[ "Username:" ],
163                        td[ input(type='text',name='username') ],
164                    ],
165                    tr[
166                        td[ "Password:" ],
167                        td[ input(type='password',name='password') ],
168                    ]
169                ],
170                input(type='submit'),
171                p,
172            ]
173        ]
174    ])
175
176class Logout(rend.Page):
177    implements(ILogout)
178
179    def logout(self, request):
180        request.getSession().expire()
181        request.setComponent(iformless.IRedirectAfterPost, "/"+guard.LOGOUT_AVATAR)
182
183    docFactory = loaders.stan(html[webform.renderForms()])
184
185class UserPage(rend.Page):
186    implements(IEditUser, IDeleteUser)
187
188    def __init__(self, user):
189        rend.Page.__init__(self)
190        self.user = user
191
192    def beforeRender(self, ctx):
193        #get user, usermanager from the session
194        self.currentUser = inevow.ISession(ctx).getComponent(ICurrentUser)
195        self.userManager = inevow.ISession(ctx).getComponent(IUserManager)
196
197    def setEmail(self, newEmail):
198        self.user.email = newEmail
199    email = property(lambda self: self.user.email, setEmail)
200
201    def setPassword(self, newPassword):
202        self.user.password = newPassword
203    password = property(lambda self: self.user.password, setPassword)
204
205    def deleteUser(self, request):
206        self.userManager = request.session.getComponent(IUserManager)
207        self.userManager.deleteUser(self.user.username)
208        request.setComponent(iformless.IRedirectAfterPost, "/")
209
210    def canModify(self):
211        #determine our capabilities
212        if self.currentUser.role == "ADMIN":
213            return True
214        if self.currentUser.username == self.user.username:
215            return True
216        return False
217
218    def render_desc(self, ctx, data):
219        args = inevow.IRequest(ctx).args
220        view = args.get('action', ['view'])[0]
221        if not self.canModify():
222            #if they don't have required role, bump to "view"
223            #regardless of what they passed in
224            view = "view"
225
226        if view == "view":
227            msg = "Viewing attributes for user: %s"
228        elif view == "edit":
229            msg = "Editing attributes for user: %s"
230        elif view == "delete":
231            msg = "Deleting user: %s"
232        else:
233            msg = "Unknown action command on user: %s"
234        return msg%self.user.username
235
236    def render_viewSelector(self, context, data):
237        args = inevow.IRequest(context).args
238        view = args.get('action', ['view'])[0]
239        if not self.canModify():
240            view = "view"
241        actionRenderer = ''
242
243        if view == 'view':
244            if not self.canModify():
245                #if they don't have required role, don't show
246                #edit/delete to them
247                selector = ""
248            else:
249                selector = "View | ", a(href=url.here.add('action','edit'))[ "Edit | " ], a(href=url.here.add('action','delete'))[ "Delete" ]
250            actionRenderer = context.onePattern('view')() # get one copy of the view pattern
251        elif view == 'edit':
252            selector = a(href=url.here.add('action','view'))["View"], " | Edit |", a(href=url.here.add('action','delete'))[ "Delete" ]
253            actionRenderer = context.onePattern('edit')() # get one copy of the edit pattern
254            actionRenderer[webform.renderForms(bindingNames=['password'])],
255            actionRenderer[webform.renderForms(bindingNames=['email'])],
256        elif view == 'delete':
257            selector = a(href=url.here.add('action','view'))["View"], a(href=url.here.add('action','edit'))[ "| Edit" ], " | Delete"
258            actionRenderer = context.onePattern('delete')() # get one copy of the delete pattern
259            actionRenderer[webform.renderForms(bindingNames=['deleteUser'])],
260        else:
261            return "Unknown view: ",view
262
263        return selector, actionRenderer
264
265
266    def render_userDetail(self, context, data):
267        return ul[
268            li["username: ",self.user.username],
269            li["email: ", self.user.email],
270            ]
271
272    docFactory = loaders.stan(html[
273    head[
274        link(href="/webform_css", rel="stylesheet")
275        ],
276    body[
277        p[
278            a(href=url.here.parent())["Up"]
279        ],
280        br(),
281        a(href="/logout")["logout"],
282        br(),
283        render_desc,
284        br(),
285        div(render=render_viewSelector)[
286            #only one of the patterns below will be
287            #rendered based on the "action" param passed
288            #in the request (default is "view")
289            p(pattern="view")[
290              render_userDetail
291            ],
292            p(pattern="edit"),
293            p(pattern="delete")
294        ]
295    ]
296    ])
297
298class UserBrowserPage(rend.Page):
299    implements(ICreateUser)
300
301    addSlash = True
302
303    # Fixed name children
304    child_logout = Logout()
305
306    def __init__(self, um, user):
307        rend.Page.__init__(self)
308        self.userManager = um
309        self.user = user #user we are logged in as
310
311    def setupSession(self, ctx):
312        #need to place the user, usermanager in the session
313        inevow.ISession(ctx).setComponent(ICurrentUser, self.user)
314        inevow.ISession(ctx).setComponent(IUserManager, self.userManager)
315
316    def beforeRender(self, ctx):
317        self.setupSession(ctx)
318
319    def childFactory(self, ctx, name):
320        #make sure our child nodes have access to these session objects
321        self.setupSession(ctx)
322
323        # name == the username of the user in the database.
324        user = self.userManager.findUser(name)
325        if user is None:
326            return None
327
328        #return page for a particular user object
329        return UserPage(user)
330
331    def createUser(self, request, username, password, email, role):
332        self.userManager.createUser(username, password, email, role=role)
333        request.setComponent(iformless.IRedirectAfterPost, "/")
334
335    def data_getAllUsers(self, context, data):
336        users = self.userManager.findAllUsers()
337        return users
338
339    def render_greeting(self, context, data):
340        return "Welcome to the UserDB browser, %s."%self.user.username
341
342    def render_user(self, context, data):
343        user = data
344        #determine our capabilities
345        canModify = False
346        if self.user.role == "ADMIN":
347            canModify =  True
348        if self.user.username == user.username:
349            canModify =  True
350
351        if canModify:
352            return context.tag[
353                user.username, " [ ",
354                a(href=user.username+"?action=view")[" view "]," | ",
355                a(href=user.username+"?action=edit")[ "edit" ]," | ",
356                a(href=user.username+"?action=delete")[ "delete" ]," ]"
357                ]
358        else:
359            return context.tag[
360                user.username, " [ ",
361                a(href=user.username+"?action=view")[" view "]," ] "
362                ]
363
364
365    def render_createForm(self, context, data):
366        #only people in ADMIN role can create new users
367        if self.user.role == "ADMIN":
368            return webform.renderForms()
369        else:
370            return ""
371
372    child_webform_css = webform.defaultCSS
373
374    docFactory = loaders.stan(html[
375        head[
376          link(href="/webform_css", rel="stylesheet")
377        ],
378        body[
379          render_greeting,
380          br(),
381          a(href="/logout")["logout"],
382          br(),
383          p[
384            span["Existing users:"]
385          ],
386          ul(data=directive("getAllUsers"), render=directive("sequence"))[
387            li(pattern="item", render=render_user)
388          ],
389          render_createForm
390        ]])
391
392#=============================================================================
393# Cred/Auth/Realm
394#=============================================================================
395def noLogout():
396    return None
397
398class SimpleChecker:
399    """
400    A simple checker implementation. Delegates storage/retrieval to userdb object
401    """
402    implements(checkers.ICredentialsChecker)
403    credentialInterfaces = (credentials.IUsernamePassword,)
404
405    def __init__(self, userdb):
406        self.userdb = userdb
407
408    #implements ICredentialChecker
409    def requestAvatarId(self, credentials):
410        """Return the avatar id of the avatar which can be accessed using
411        the given credentials.
412
413        credentials will be an object with username and password attributes
414        we need to raise an error to indicate failure or return a username
415        to indicate success. requestAvatar will then be called with the avatar
416        id we returned.
417        """
418        user = self.userdb.findUser(credentials.username)
419        if user is not None:
420            return defer.maybeDeferred(
421                credentials.checkPassword, user.password).addCallback(
422                self._cbPasswordMatch, credentials.username)
423        else:
424            print "No user named: ",credentials.username
425            raise error.UnauthorizedLogin()
426
427    def _cbPasswordMatch(self, matched, username):
428        if matched:
429            return username
430        else:
431            print "password didn't match: ",username
432            return failure.Failure(error.UnauthorizedLogin())
433
434class SimpleRealm:
435    """A simple implementor of cred's IRealm.
436    For web, this gives us the LoggedIn page.
437    """
438    implements(portal.IRealm)
439
440    def __init__(self, userdb):
441        #we need this to pass into UserDB page
442        self.userdb = userdb
443
444    #implements IRealm
445    def requestAvatar(self, avatarId, mind, *interfaces):
446        for iface in interfaces:
447            if iface is inevow.IResource:
448                # do web stuff
449                if avatarId is checkers.ANONYMOUS:
450                    resc = NotLoggedIn()
451                    resc.realm = self
452                    return (inevow.IResource, resc, noLogout)
453                else:
454                    user = self.userdb.findUser(avatarId)
455                    resc = UserBrowserPage(self.userdb, user)
456                    resc.realm = self
457                    return (inevow.IResource, resc, noLogout)
458
459        raise NotImplementedError("Can't support that interface.")
460
461#=============================================================================
462# Initialization
463#=============================================================================
464def main():
465    userdb = UserDictDB()
466    userdb.users["admin"] = User("admin", "admin", "admin@blah.com", role="ADMIN")
467    userdb.users["foo"] = User("foo", "foo", "foo@blah.com")
468    userdb.users["bar"] = User("bar", "bar", "bar@blah.com")
469
470
471    realm = SimpleRealm(userdb)
472    ptl = portal.Portal(realm)
473    myChecker = SimpleChecker(userdb)
474    # Allow anonymous access.  Needed for access to NotLoggedIn
475    ptl.registerChecker(checkers.AllowAnonymousAccess(), credentials.IAnonymous)
476    # Allow users registered in the userdb
477    ptl.registerChecker(myChecker)
478
479    site = appserver.NevowSite(
480        resource=guard.SessionWrapper(ptl)
481    )
482    return strports.service("8080", site)
483
484application = service.Application("UserManager1")
485main().setServiceParent(application)
486