| 23 | | @TODO |
|---|
| 24 | | |
|---|
| 25 | | |
|---|
| 26 | | Implementing it in an existing project |
|---|
| 27 | | -------------------------------------- |
|---|
| 28 | | @TODO |
|---|
| | 74 | |
|---|
| | 75 | Your auth-related model doesn't `have to` be like the default one, where the |
|---|
| | 76 | class for your users, groups and permissions are, respectively, ``User``, |
|---|
| | 77 | ``Group`` and ``Permission``, and your users' user name is available in |
|---|
| | 78 | ``User.user_name``. What if you prefer ``Member`` and ``Team`` instead of |
|---|
| | 79 | ``User`` and ``Group``, respectively? Or what if you prefer ``Group.members`` |
|---|
| | 80 | instead of ``Group.users``? You're in the right place! |
|---|
| | 81 | |
|---|
| | 82 | Changing class names |
|---|
| | 83 | ~~~~~~~~~~~~~~~~~~~~ |
|---|
| | 84 | |
|---|
| | 85 | Changing the name of an auth-related class (``User``, ``Group`` or ``Permission``) |
|---|
| | 86 | is a rather simple task. Just rename it in your model, and then make sure to |
|---|
| | 87 | update ``{yourproject}.config.app_cfg`` accordingly. |
|---|
| | 88 | |
|---|
| | 89 | For example, if you renamed ``User`` to ``Member``, ``{yourproject}.config.app_cfg`` |
|---|
| | 90 | should look like this:: |
|---|
| | 91 | |
|---|
| | 92 | # ... |
|---|
| | 93 | from yourproject import model |
|---|
| | 94 | # ... |
|---|
| | 95 | base_config.sa_auth.user_class = model.Member |
|---|
| | 96 | # ... |
|---|
| | 97 | |
|---|
| | 98 | Changing attribute names |
|---|
| | 99 | ~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| | 100 | |
|---|
| | 101 | You can also change the name of the attributes assumed by |
|---|
| | 102 | :mod:`tgext.authorization` in your auth-related classes, such as renaming |
|---|
| | 103 | ``User.groups`` by ``User.memberships``. |
|---|
| | 104 | |
|---|
| | 105 | Changing such values is what :mod:`tgext.authorization` calls "translating". |
|---|
| | 106 | You may set the translations for the attributes of the models |
|---|
| | 107 | :mod:`tgext.authorization` deals with in ``{yourproject}.config.app_cfg``. For |
|---|
| | 108 | example, if you want to replace ``Group.users`` by ``Group.members``, you may |
|---|
| | 109 | set the following translation in that file:: |
|---|
| | 110 | |
|---|
| | 111 | base_config.sa_auth.translations.users = 'members' |
|---|
| | 112 | |
|---|
| | 113 | These are the translations you may set in ``base_config.sa_auth.translations``: |
|---|
| | 114 | * ``user_name``: The translation for the attribute in ``User.user_name``. |
|---|
| | 115 | * ``users``: The translation for the attribute in ``Group.users``. |
|---|
| | 116 | * ``group_name``: The translation for the attribute in ``Group.group_name``. |
|---|
| | 117 | * ``groups``: The translation for the attribute in ``User.groups`` and |
|---|
| | 118 | ``Permission.groups``. |
|---|
| | 119 | * ``permission_name``: The translation for the attribute in |
|---|
| | 120 | ``Permission.permission_name``. |
|---|
| | 121 | * ``permissions``: The translation for the attribute in ``User.permissions`` |
|---|
| | 122 | and ``Group.permissions``. |
|---|
| | 123 | * ``validate_password``: The translation for the method in |
|---|
| | 124 | ``User.validate_password``. |
|---|
| | 125 | |
|---|
| | 126 | |
|---|
| | 127 | .. _implementing: |
|---|
| | 128 | |
|---|
| | 129 | Enabling the quickstart in an existing project |
|---|
| | 130 | ---------------------------------------------- |
|---|
| | 131 | |
|---|
| | 132 | To enable authentication and authorization via :mod:`tgext.authorization`'s |
|---|
| | 133 | quickstart, you should follow the instructions described in this section: |
|---|
| | 134 | |
|---|
| | 135 | #. Go to ``{yourproject}.config.app_cfg`` and define the following settings: |
|---|
| | 136 | * ``base_config.auth_backend``: The name of the |
|---|
| | 137 | authentication/authorization backend. Set it to "sqlalchemy". |
|---|
| | 138 | * ``base_config.sa_auth.dbsession``: Your model's SQLAlchemy session. |
|---|
| | 139 | * ``base_config.sa_auth.user_class``: Your user class. |
|---|
| | 140 | * ``base_config.sa_auth.group_class``: Your group class. |
|---|
| | 141 | * ``base_config.sa_auth.permission_class``: Your permission class. |
|---|
| | 142 | |
|---|
| | 143 | It may look like this:: |
|---|
| | 144 | |
|---|
| | 145 | # ... |
|---|
| | 146 | from yourproject import model |
|---|
| | 147 | # ... |
|---|
| | 148 | base_config.auth_backend = 'sqlalchemy' |
|---|
| | 149 | base_config.sa_auth.dbsession = model.DBSession |
|---|
| | 150 | base_config.sa_auth.user_class = model.User |
|---|
| | 151 | base_config.sa_auth.group_class = model.Group |
|---|
| | 152 | base_config.sa_auth.permission_class = model.Permission |
|---|
| | 153 | # ... |
|---|
| | 154 | |
|---|
| | 155 | #. Now define your auth-related data model in, say, |
|---|
| | 156 | ``{yourproject}.model.auth``, with at least the definitions below (you |
|---|
| | 157 | may add more columns if you want):: |
|---|
| | 158 | |
|---|
| | 159 | import md5 |
|---|
| | 160 | import sha |
|---|
| | 161 | from datetime import datetime |
|---|
| | 162 | |
|---|
| | 163 | from tg import config |
|---|
| | 164 | from sqlalchemy import Table, ForeignKey, Column |
|---|
| | 165 | from sqlalchemy.types import String, Unicode, UnicodeText, Integer, DateTime, \ |
|---|
| | 166 | Boolean, Float |
|---|
| | 167 | from sqlalchemy.orm import relation, backref, synonym |
|---|
| | 168 | |
|---|
| | 169 | from yourproject.model import DeclarativeBase, metadata, DBSession |
|---|
| | 170 | |
|---|
| | 171 | |
|---|
| | 172 | # This is the association table for the many-to-many relationship between |
|---|
| | 173 | # groups and permissions. |
|---|
| | 174 | group_permission_table = Table('tg_group_permission', metadata, |
|---|
| | 175 | Column('group_id', Integer, ForeignKey('tg_group.group_id', |
|---|
| | 176 | onupdate="CASCADE", ondelete="CASCADE")), |
|---|
| | 177 | Column('permission_id', Integer, ForeignKey('tg_permission.permission_id', |
|---|
| | 178 | onupdate="CASCADE", ondelete="CASCADE")) |
|---|
| | 179 | ) |
|---|
| | 180 | |
|---|
| | 181 | # This is the association table for the many-to-many relationship between |
|---|
| | 182 | # groups and members - this is, the memberships. |
|---|
| | 183 | user_group_table = Table('tg_user_group', metadata, |
|---|
| | 184 | Column('user_id', Integer, ForeignKey('tg_user.user_id', |
|---|
| | 185 | onupdate="CASCADE", ondelete="CASCADE")), |
|---|
| | 186 | Column('group_id', Integer, ForeignKey('tg_group.group_id', |
|---|
| | 187 | onupdate="CASCADE", ondelete="CASCADE")) |
|---|
| | 188 | ) |
|---|
| | 189 | |
|---|
| | 190 | # auth model |
|---|
| | 191 | |
|---|
| | 192 | class Group(DeclarativeBase): |
|---|
| | 193 | """An ultra-simple group definition. |
|---|
| | 194 | """ |
|---|
| | 195 | __tablename__ = 'tg_group' |
|---|
| | 196 | |
|---|
| | 197 | group_id = Column(Integer, autoincrement=True, primary_key=True) |
|---|
| | 198 | |
|---|
| | 199 | group_name = Column(Unicode(16), unique=True) |
|---|
| | 200 | |
|---|
| | 201 | display_name = Column(Unicode(255)) |
|---|
| | 202 | |
|---|
| | 203 | created = Column(DateTime, default=datetime.now) |
|---|
| | 204 | |
|---|
| | 205 | users = relation('User', secondary=user_group_table, backref='groups') |
|---|
| | 206 | |
|---|
| | 207 | def __repr__(self): |
|---|
| | 208 | return '<Group: name=%s>' % self.group_name |
|---|
| | 209 | |
|---|
| | 210 | |
|---|
| | 211 | class User(DeclarativeBase): |
|---|
| | 212 | """Reasonably basic User definition. Probably would want additional |
|---|
| | 213 | attributes. |
|---|
| | 214 | """ |
|---|
| | 215 | __tablename__ = 'tg_user' |
|---|
| | 216 | |
|---|
| | 217 | user_id = Column(Integer, autoincrement=True, primary_key=True) |
|---|
| | 218 | |
|---|
| | 219 | user_name = Column(Unicode(16), unique=True) |
|---|
| | 220 | |
|---|
| | 221 | email_address = Column(Unicode(255), unique=True) |
|---|
| | 222 | |
|---|
| | 223 | display_name = Column(Unicode(255)) |
|---|
| | 224 | |
|---|
| | 225 | _password = Column('password', Unicode(40)) |
|---|
| | 226 | |
|---|
| | 227 | created = Column(DateTime, default=datetime.now) |
|---|
| | 228 | |
|---|
| | 229 | def __repr__(self): |
|---|
| | 230 | return '<User: email="%s", display name="%s">' % ( |
|---|
| | 231 | self.email_address, self.display_name) |
|---|
| | 232 | |
|---|
| | 233 | @property |
|---|
| | 234 | def permissions(self): |
|---|
| | 235 | perms = set() |
|---|
| | 236 | for g in self.groups: |
|---|
| | 237 | perms = perms | set(g.permissions) |
|---|
| | 238 | return perms |
|---|
| | 239 | |
|---|
| | 240 | def _set_password(self, password): |
|---|
| | 241 | """encrypts password on the fly using the encryption |
|---|
| | 242 | algo defined in the configuration |
|---|
| | 243 | """ |
|---|
| | 244 | algorithm = self.get_encryption_method() |
|---|
| | 245 | self._password = self.__encrypt_password(algorithm, password) |
|---|
| | 246 | |
|---|
| | 247 | def _get_password(self): |
|---|
| | 248 | """returns password |
|---|
| | 249 | """ |
|---|
| | 250 | return self._password |
|---|
| | 251 | |
|---|
| | 252 | password = synonym('password', descriptor=property(_get_password, |
|---|
| | 253 | _set_password)) |
|---|
| | 254 | |
|---|
| | 255 | def __encrypt_password(self, algorithm, password): |
|---|
| | 256 | """Hash the given password with the specified algorithm. Valid values |
|---|
| | 257 | for algorithm are 'md5' and 'sha1'. All other algorithm values will |
|---|
| | 258 | be essentially a no-op.""" |
|---|
| | 259 | hashed_password = password |
|---|
| | 260 | |
|---|
| | 261 | if isinstance(password, unicode): |
|---|
| | 262 | password_8bit = password.encode('UTF-8') |
|---|
| | 263 | |
|---|
| | 264 | else: |
|---|
| | 265 | password_8bit = password |
|---|
| | 266 | |
|---|
| | 267 | if "md5" == algorithm: |
|---|
| | 268 | hashed_password = md5.new(password_8bit).hexdigest() |
|---|
| | 269 | |
|---|
| | 270 | elif "sha1" == algorithm: |
|---|
| | 271 | hashed_password = sha.new(password_8bit).hexdigest() |
|---|
| | 272 | |
|---|
| | 273 | # TODO: re-add the possibility to provide own hasing algo |
|---|
| | 274 | # here... just get the real config... |
|---|
| | 275 | |
|---|
| | 276 | #elif "custom" == algorithm: |
|---|
| | 277 | # custom_encryption_path = turbogears.config.get( |
|---|
| | 278 | # "auth.custom_encryption", None ) |
|---|
| | 279 | # |
|---|
| | 280 | # if custom_encryption_path: |
|---|
| | 281 | # custom_encryption = turbogears.util.load_class( |
|---|
| | 282 | # custom_encryption_path) |
|---|
| | 283 | |
|---|
| | 284 | # if custom_encryption: |
|---|
| | 285 | # hashed_password = custom_encryption(password_8bit) |
|---|
| | 286 | |
|---|
| | 287 | # make sure the hased password is an UTF-8 object at the end of the |
|---|
| | 288 | # process because SQLAlchemy _wants_ a unicode object for Unicode columns |
|---|
| | 289 | if not isinstance(hashed_password, unicode): |
|---|
| | 290 | hashed_password = hashed_password.decode('UTF-8') |
|---|
| | 291 | |
|---|
| | 292 | return hashed_password |
|---|
| | 293 | |
|---|
| | 294 | def get_encryption_method(self): |
|---|
| | 295 | """returns the encryption method from the config |
|---|
| | 296 | If None is set, or auth is disabled this will return None |
|---|
| | 297 | """ |
|---|
| | 298 | auth_system = config.get('sa_auth', None) |
|---|
| | 299 | if auth_system is None: |
|---|
| | 300 | # if auth is not activated in the config we should warn |
|---|
| | 301 | # the admin through the logs... and return None |
|---|
| | 302 | return None |
|---|
| | 303 | |
|---|
| | 304 | return auth_system.get('password_encryption_method', None) |
|---|
| | 305 | |
|---|
| | 306 | def validate_password(self, password): |
|---|
| | 307 | """Check the password against existing credentials. |
|---|
| | 308 | this method _MUST_ return a boolean. |
|---|
| | 309 | |
|---|
| | 310 | @param password: the password that was provided by the user to |
|---|
| | 311 | try and authenticate. This is the clear text version that we will |
|---|
| | 312 | need to match against the (possibly) encrypted one in the database. |
|---|
| | 313 | @type password: unicode object |
|---|
| | 314 | """ |
|---|
| | 315 | algorithm = self.get_encryption_method() |
|---|
| | 316 | return self.password == self.__encrypt_password(algorithm, password) |
|---|
| | 317 | |
|---|
| | 318 | |
|---|
| | 319 | class Permission(DeclarativeBase): |
|---|
| | 320 | """A relationship that determines what each Group can do""" |
|---|
| | 321 | __tablename__ = 'tg_permission' |
|---|
| | 322 | |
|---|
| | 323 | permission_id = Column(Integer, autoincrement=True, primary_key=True) |
|---|
| | 324 | |
|---|
| | 325 | permission_name = Column(Unicode(16), unique=True) |
|---|
| | 326 | |
|---|
| | 327 | description = Column(Unicode(255)) |
|---|
| | 328 | |
|---|
| | 329 | groups = relation(Group, secondary=group_permission_table, |
|---|
| | 330 | backref='permissions') |
|---|
| | 331 | |
|---|
| | 332 | Finally, make sure these classes are imported at the end of your |
|---|
| | 333 | ``{yourproject}/model/__init__.py``:: |
|---|
| | 334 | |
|---|
| | 335 | from auth import User, Group, Permission |
|---|
| | 336 | |
|---|
| | 337 | #. Finally, you may want to create some default users, groups and permissions |
|---|
| | 338 | to try authorization in your application. In ``{yourproject}.websetup`` |
|---|
| | 339 | you may add a code like this in your ``setup_config()`` function:: |
|---|
| | 340 | |
|---|
| | 341 | # ... |
|---|
| | 342 | |
|---|
| | 343 | model.metadata.create_all(bind=config['pylons.app_globals'].sa_engine) |
|---|
| | 344 | |
|---|
| | 345 | u = model.User() |
|---|
| | 346 | u.user_name = u'manager' |
|---|
| | 347 | u.display_name = u'Example manager' |
|---|
| | 348 | u.email_address = u'manager@somedomain.com' |
|---|
| | 349 | u.password = u'managepass' |
|---|
| | 350 | |
|---|
| | 351 | model.DBSession.save(u) |
|---|
| | 352 | |
|---|
| | 353 | g = model.Group() |
|---|
| | 354 | g.group_name = u'managers' |
|---|
| | 355 | g.display_name = u'Managers Group' |
|---|
| | 356 | |
|---|
| | 357 | g.users.append(u) |
|---|
| | 358 | |
|---|
| | 359 | model.DBSession.save(g) |
|---|
| | 360 | |
|---|
| | 361 | p = model.Permission() |
|---|
| | 362 | p.permission_name = u'manage' |
|---|
| | 363 | p.description = u'This permission give an administrative right to the bearer' |
|---|
| | 364 | p.groups.append(g) |
|---|
| | 365 | |
|---|
| | 366 | model.DBSession.save(p) |
|---|
| | 367 | model.DBSession.flush() |
|---|
| | 368 | |
|---|
| | 369 | u1 = model.User() |
|---|
| | 370 | u1.user_name = u'editor' |
|---|
| | 371 | u1.display_name = u'Exemple editor' |
|---|
| | 372 | u1.email_address = u'editor@somedomain.com' |
|---|
| | 373 | u1.password = u'editpass' |
|---|
| | 374 | |
|---|
| | 375 | model.DBSession.save(u1) |
|---|
| | 376 | model.DBSession.flush() |
|---|
| | 377 | |
|---|
| | 378 | transaction.commit() |
|---|
| | 379 | print "Successfully setup" |
|---|
| | 380 | |
|---|
| | 381 | And then populate your test database with these rows. To do so, first |
|---|
| | 382 | delete the file ``devdata.db`` from your project's root directory, and |
|---|
| | 383 | finally run the command below from the same directory:: |
|---|
| | 384 | |
|---|
| | 385 | paster setup-app development.ini |
|---|
| | 386 | |
|---|
| | 387 | |
|---|
| | 388 | Disabling the quickstart |
|---|
| | 389 | ------------------------ |
|---|
| | 390 | |
|---|
| | 391 | If you need more flexibility than that provided by the quickstart, you may |
|---|
| | 392 | disable it by removing (or commenting) the following line from |
|---|
| | 393 | ``{yourproject}.config.app_cfg``:: |
|---|
| | 394 | |
|---|
| | 395 | base_config.auth_backend = 'sqlalchemy' |
|---|
| | 396 | |
|---|
| | 397 | Then you may also want to delete those settings like ``base_config.sa_auth.*``, |
|---|
| | 398 | because they'll be ignored. |
|---|