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