Package turbogears :: Package identity :: Module saprovider

Source Code for Module turbogears.identity.saprovider

  1  import logging 
  2  from datetime import datetime 
  3   
  4  from turbogears import config, identity 
  5  from turbogears.database import bind_metadata, metadata, session 
  6  from turbogears.util import load_class 
  7  from turbojson.jsonify import jsonify_saobject, jsonify 
  8   
  9  from sqlalchemy import (Table, Column, ForeignKey, 
 10      String, Unicode, Integer, DateTime) 
 11  from sqlalchemy.orm import class_mapper, mapper, relation 
 12  try: 
 13      from sqlalchemy.exc import IntegrityError 
 14  except ImportError: # SQLAlchemy < 0.5 
 15      from sqlalchemy.exceptions import IntegrityError 
 16  try: 
 17      from sqlalchemy.orm.exc import UnmappedClassError 
 18  except ImportError: # SQLAlchemy < 0.5 
 19      from sqlalchemy.exceptions import InvalidRequestError as UnmappedClassError 
 20   
 21  log = logging.getLogger('turbogears.identity.saprovider') 
 22   
 23   
 24  # Global class references -- 
 25  # these will be set when the provider is initialized. 
 26  user_class = None 
 27  group_class = None 
 28  permission_class = None 
 29  visit_class = None 
30 31 32 -class SqlAlchemyIdentity(object):
33 """Identity that uses a model from a database (via SQLAlchemy).""" 34
35 - def __init__(self, visit_key=None, user=None):
36 self.visit_key = visit_key 37 if user: 38 self._user = user 39 if visit_key is not None: 40 self.login()
41 42 @property
43 - def user(self):
44 """Get user instance for this identity.""" 45 try: 46 return self._user 47 except AttributeError: 48 # User hasn't already been set 49 pass 50 # Attempt to load the user. After this code executes, there *will* be 51 # a _user attribute, even if the value is None. 52 visit = self.visit_link 53 try: 54 self._user = visit and session.query(user_class).get(visit.user_id) 55 except TypeError: 56 # User is gone 57 return None 58 return self._user
59 60 @property
61 - def user_name(self):
62 """Get user name of this identity.""" 63 if not self.user: 64 return None 65 return self.user.user_name
66 67 @property
68 - def user_id(self):
69 """Get user id of this identity.""" 70 if not self.user: 71 return None 72 return self.user.user_id
73 74 @property
75 - def anonymous(self):
76 """Return true if not logged in.""" 77 return not self.user
78 79 @property
80 - def permissions(self):
81 """Get set of permission names of this identity.""" 82 try: 83 return self._permissions 84 except AttributeError: 85 # Permissions haven't been computed yet 86 pass 87 if not self.user: 88 self._permissions = frozenset() 89 else: 90 self._permissions = frozenset( 91 p.permission_name for p in self.user.permissions) 92 return self._permissions
93 94 @property
95 - def groups(self):
96 """Get set of group names of this identity.""" 97 try: 98 return self._groups 99 except AttributeError: 100 # Groups haven't been computed yet 101 pass 102 if not self.user: 103 self._groups = frozenset() 104 else: 105 self._groups = frozenset(g.group_name for g in self.user.groups) 106 return self._groups
107 108 @property
109 - def group_ids(self):
110 """Get set of group IDs of this identity.""" 111 try: 112 return self._group_ids 113 except AttributeError: 114 # Groups haven't been computed yet 115 pass 116 if not self.user: 117 self._group_ids = frozenset() 118 else: 119 self._group_ids = frozenset(g.group_id for g in self.user.groups) 120 return self._group_ids
121 122 @property 129 130 @property
131 - def login_url(self):
132 """Get the URL for the login page.""" 133 return identity.get_failure_url()
134
135 - def login(self):
136 """Set the link between this identity and the visit.""" 137 visit = self.visit_link 138 if not visit: 139 visit = visit_class() 140 visit.visit_key = self.visit_key 141 session.add(visit) 142 visit.user_id = self._user.user_id 143 try: 144 session.flush() 145 except IntegrityError: 146 visit = self.visit_link 147 if not visit: 148 raise 149 visit.user_id = self._user.user_id 150 session.flush()
151
152 - def logout(self):
153 """Remove the link between this identity and the visit.""" 154 visit = self.visit_link 155 if visit: 156 session.delete(visit) 157 session.flush() 158 # Clear the current identity 159 identity.set_current_identity(SqlAlchemyIdentity())
160
161 162 -class SqlAlchemyIdentityProvider(object):
163 """IdentityProvider that uses a model from a database (via SQLAlchemy).""" 164
165 - def __init__(self):
166 super(SqlAlchemyIdentityProvider, self).__init__() 167 glob_ns = globals() 168 169 for classname in ('user', 'group', 'permission', 'visit'): 170 default_classname = '.TG_' + (classname == 'visit' 171 and 'VisitIdentity' or classname.capitalize()) 172 class_path = config.get('identity.saprovider.model.%s' % classname, 173 __name__ + default_classname) 174 class_ = load_class(class_path) 175 if class_: 176 if class_path == __name__ + default_classname: 177 try: 178 class_mapper(class_) 179 except UnmappedClassError: 180 class_._map() 181 log.info('Successfully loaded "%s".', class_path) 182 glob_ns['%s_class' % classname] = class_ 183 else: 184 log.error('Could not load class "%s". Check ' 185 'identity.saprovider.model.%s setting', class_path, classname)
186
187 - def encrypt_password(self, password):
188 # Default encryption algorithm is to use plain text passwords 189 algorithm = config.get('identity.saprovider.encryption_algorithm', None) 190 return identity.encrypt_pw_with_algorithm(algorithm, password)
191
192 - def create_provider_model(self):
193 """Create the database tables if they don't already exist.""" 194 bind_metadata() 195 class_mapper(user_class).local_table.create(checkfirst=True) 196 class_mapper(group_class).local_table.create(checkfirst=True) 197 if group_class is TG_Group: 198 group_class._user_association_table.create(checkfirst=True) 199 class_mapper(permission_class).local_table.create(checkfirst=True) 200 if permission_class is TG_Permission: 201 permission_class._group_association_table.create(checkfirst=True) 202 class_mapper(visit_class).local_table.create(checkfirst=True)
203
204 - def validate_identity(self, user_name, password, visit_key):
205 """Validate the identity represented by user_name using the password. 206 207 Must return either None if the credentials weren't valid or an object 208 with the following properties: 209 user_name: original user name 210 user: a provider dependent object (TG_User or similar) 211 groups: a set of group names 212 permissions: a set of permission names 213 214 """ 215 user = session.query(user_class).filter_by(user_name=user_name).first() 216 217 if not user: 218 log.warning("No such user: %s", user_name) 219 return None 220 221 if not self.validate_password(user, user_name, password): 222 log.info("Passwords don't match for user: %s", user_name) 223 return None 224 225 log.info("Associating user (%s) with visit (%s)", user_name, visit_key) 226 return SqlAlchemyIdentity(visit_key, user)
227
228 - def validate_password(self, user, user_name, password):
229 """Check the user_name and password against existing credentials. 230 231 Note: user_name is not used here, but is required by external 232 password validation schemes that might override this method. 233 If you use SqlAlchemyIdentityProvider, but want to check the passwords 234 against an external source (i.e. PAM, LDAP, Windows domain, etc), 235 subclass SqlAlchemyIdentityProvider, and override this method. 236 237 """ 238 return user.password == self.encrypt_password(password)
239
240 - def load_identity(self, visit_key):
241 """Lookup the principal represented by user_name. 242 243 Return None if there is no principal for the given user ID. 244 245 Must return an object with the following properties: 246 user_name: original user name 247 user: a provider dependent object (TG_User or similar) 248 groups: a set of group names 249 permissions: a set of permission names 250 251 """ 252 return SqlAlchemyIdentity(visit_key)
253
254 - def anonymous_identity(self):
255 """Return anonymous identity. 256 257 Must return an object with the following properties: 258 user_name: original user name 259 user: a provider dependent object (TG_User or similar) 260 groups: a set of group names 261 permissions: a set of permission names 262 263 """ 264 return SqlAlchemyIdentity()
265
266 - def authenticated_identity(self, user):
267 """Construct Identity object for users with no visit_key.""" 268 return SqlAlchemyIdentity(user=user)
269
270 271 # default identity model classes 272 273 274 -class TG_User(object):
275 """Reasonably basic User definition.""" 276
277 - def __repr__(self):
278 return '<User: name="%s", email="%s", display name="%s">' % ( 279 self.user_name, self.email_address, self.display_name)
280
281 - def __unicode__(self):
282 return self.display_name or self.user_name
283 284 @property
285 - def permissions(self):
286 """Return all permissions of all groups the user belongs to.""" 287 p = set() 288 for g in self.groups: 289 p |= set(g.permissions) 290 return p
291 292 @classmethod
293 - def by_email_address(cls, email_address):
294 return session.query(cls).filter_by(email_address=email_address).first()
295 296 @classmethod
297 - def by_user_name(cls, user_name):
298 return session.query(cls).filter_by(user_name=user_name).first()
299 by_name = by_user_name 300
301 - def _set_password(self, cleartext_password):
302 """Run cleartext password through the hash algorithm before saving.""" 303 try: 304 hash = identity.current_provider.encrypt_password(cleartext_password) 305 except identity.exceptions.IdentityManagementNotEnabledException: 306 # Creating identity provider just to encrypt password 307 # (so we don't reimplement the encryption step). 308 ip = SqlAlchemyIdentityProvider() 309 hash = ip.encrypt_password(cleartext_password) 310 if hash == cleartext_password: 311 log.info("Identity provider not enabled," 312 " and no encryption algorithm specified in config." 313 " Setting password as plaintext.") 314 if isinstance(hash, str): 315 hash = unicode(hash) 316 self._password = hash
317
318 - def _get_password(self):
319 """Returns password.""" 320 return self._password
321 322 password = property(_get_password, _set_password) 323
324 - def set_password_raw(self, password):
325 """Save the password as-is to the database.""" 326 if isinstance(password, str): 327 hash = unicode(password) 328 self._password = password
329 330 @classmethod
331 - def _map(cls):
332 cls._table = Table('tg_user', metadata, 333 Column('user_id', Integer, primary_key=True), 334 Column('user_name', Unicode(16), unique=True, nullable=False), 335 Column('email_address', Unicode(255), unique=True), 336 Column('display_name', Unicode(255)), 337 Column('password', Unicode(40)), 338 Column('created', DateTime, default=datetime.now)) 339 cls._mapper = mapper(cls, cls._table, 340 properties=dict(_password=cls._table.c.password))
341
342 @jsonify.when('isinstance(obj, TG_User)') 343 -def jsonify_user(obj):
344 """Convert user to JSON.""" 345 result = jsonify_saobject(obj) 346 result.pop('password', None) 347 result.pop('_password', None) 348 result['groups'] = [g.group_name for g in obj.groups] 349 result['permissions'] = [p.permission_name for p in obj.permissions] 350 return result
351
352 353 -class TG_Group(object):
354 """An ultra-simple Group definition.""" 355
356 - def __repr__(self):
357 return '<Group: name="%s", display_name="%s">' % ( 358 self.group_name, self.display_name)
359
360 - def __unicode__(self):
361 return self.display_name or self.group_name
362 363 @classmethod
364 - def by_group_name(cls, group_name):
365 """Look up Group by given group name.""" 366 return session.query(cls).filter_by(group_name=group_name).first()
367 by_name = by_group_name 368 369 @classmethod
370 - def _map(cls):
371 cls._table = Table('tg_group', metadata, 372 Column('group_id', Integer, primary_key=True), 373 Column('group_name', Unicode(16), unique=True, nullable=False), 374 Column('display_name', Unicode(255)), 375 Column('created', DateTime, default=datetime.now)) 376 cls._user_association_table = Table('user_group', metadata, 377 Column('user_id', Integer, ForeignKey('tg_user.user_id', 378 onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), 379 Column('group_id', Integer, ForeignKey('tg_group.group_id', 380 onupdate='CASCADE', ondelete='CASCADE'), primary_key=True)) 381 cls._mapper = mapper(cls, cls._table, 382 properties=dict(users=relation(TG_User, 383 secondary=cls._user_association_table, backref='groups')))
384
385 @jsonify.when('isinstance(obj, TG_Group)') 386 -def jsonify_group(obj):
387 """Convert group to JSON.""" 388 result = jsonify_saobject(obj) 389 result['users'] = [u.user_name for u in obj.users] 390 result['permissions'] = [p.permission_name for p in obj.permissions] 391 return result
392
393 394 -class TG_Permission(object):
395 """A relationship that determines what each Group can do.""" 396
397 - def __repr__(self):
398 return '<Permission: name="%s">' % self.permission_name
399
400 - def __unicode__(self):
401 return self.permission_name
402 403 @classmethod
404 - def by_permission_name(cls, permission_name):
405 """Look up Permission by given permission name.""" 406 return session.query(cls).filter_by(permission_name=permission_name).first()
407 by_name = by_permission_name 408 409 @classmethod
410 - def _map(cls):
411 cls._table = Table('permission', metadata, 412 Column('permission_id', Integer, primary_key=True), 413 Column('permission_name', Unicode(16), unique=True, nullable=False), 414 Column('description', Unicode(255))) 415 cls._group_association_table = Table('group_permission', metadata, 416 Column('group_id', Integer, ForeignKey('tg_group.group_id', 417 onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), 418 Column('permission_id', 419 Integer, ForeignKey('permission.permission_id', 420 onupdate='CASCADE', ondelete='CASCADE'), primary_key=True)) 421 cls._mapper = mapper(cls, cls._table, 422 properties=dict(groups=relation(TG_Group, 423 secondary=cls._group_association_table, backref='permissions')))
424
425 @jsonify.when('isinstance(obj, TG_Permission)') 426 -def jsonify_permission(obj):
427 """Convert permissions to JSON.""" 428 result = jsonify_saobject(obj) 429 result['groups'] = [g.group_name for g in obj.groups] 430 return result
431
432 433 -class TG_VisitIdentity(object):
434 """A Visit that is linked to a User object.""" 435 436 @classmethod
437 - def by_visit_key(cls, visit_key):
438 """Look up VisitIdentity by given visit key.""" 439 return session.query(cls).get(visit_key)
440 441 @classmethod
442 - def _map(cls):
443 cls._table = Table('visit_identity', metadata, 444 Column('visit_key', String(40), primary_key=True), 445 Column('user_id', Integer, 446 ForeignKey('tg_user.user_id'), index=True)) 447 cls._mapper = mapper(cls, cls._table, 448 properties=dict(user=relation(TG_User, backref='visit_identity')))
449
450 451 -def encrypt_password(cleartext_password):
452 """Encrypt given cleartext password.""" 453 try: 454 hash = identity.current_provider.encrypt_password(cleartext_password) 455 except identity.exceptions.RequestRequiredException: 456 # Creating identity provider just to encrypt password 457 # (so we don't reimplement the encryption step). 458 ip = SqlAlchemyIdentityProvider() 459 hash = ip.encrypt_password(cleartext_password) 460 if hash == cleartext_password: 461 log.info("Identity provider not enabled, and no encryption " 462 "algorithm specified in config. Setting password as plaintext.") 463 return hash
464