codekingpro/portable-devtools
114k
1# -*- coding: utf-8 -*-2"""3 flask_principal4 ~~~~~~~~~~~~~~~5 6 Identity management for Flask.7 8 :copyright: (c) 2012 by Ali Afshar.9 :license: MIT, see LICENSE for more details.10 11"""12 13from __future__ import with_statement14 15__version__ = '0.4.0'16 17import sys18 19from functools import partial, wraps20from collections import deque21 22from collections import namedtuple23 24from flask import g, session, current_app, abort, request25from flask.signals import Namespace26 27PY3 = sys.version_info[0] == 328 29signals = Namespace()30 31 32identity_changed = signals.signal('identity-changed', doc="""33Signal sent when the identity for a request has been changed.34 35Actual name: ``identity-changed``36 37Authentication providers should send this signal when authentication has been38successfully performed. Flask-Principal connects to this signal and39causes the identity to be saved in the session.40 41For example::42 43 from flaskext.principal import Identity, identity_changed44 45 def login_view(req):46 username = req.form.get('username')47 # check the credentials48 identity_changed.send(app, identity=Identity(username))49""")50 51 52identity_loaded = signals.signal('identity-loaded', doc="""53Signal sent when the identity has been initialised for a request.54 55Actual name: ``identity-loaded``56 57Identity information providers should connect to this signal to perform two58major activities:59 60 1. Populate the identity object with the necessary authorization provisions.61 2. Load any additional user information.62 63For example::64 65 from flaskext.principal import identity_loaded, RoleNeed, UserNeed66 67 @identity_loaded.connect68 def on_identity_loaded(sender, identity):69 # Get the user information from the db70 user = db.get(identity.name)71 # Update the roles that a user can provide72 for role in user.roles:73 identity.provides.add(RoleNeed(role.name))74 # Save the user somewhere so we only look it up once75 identity.user = user76""")77 78 79Need = namedtuple('Need', ['method', 'value'])80"""A required need81 82This is just a named tuple, and practically any tuple will do.83 84The ``method`` attribute can be used to look up element 0, and the ``value``85attribute can be used to look up element 1.86"""87 88 89UserNeed = partial(Need, 'id')90UserNeed.__doc__ = """A need with the method preset to `"id"`."""91 92 93RoleNeed = partial(Need, 'role')94RoleNeed.__doc__ = """A need with the method preset to `"role"`."""95 96 97TypeNeed = partial(Need, 'type')98TypeNeed.__doc__ = """A need with the method preset to `"type"`."""99 100 101ActionNeed = partial(Need, 'action')102TypeNeed.__doc__ = """A need with the method preset to `"action"`."""103 104 105ItemNeed = namedtuple('ItemNeed', ['method', 'value', 'type'])106"""A required item need107 108An item need is just a named tuple, and practically any tuple will do. In109addition to other Needs, there is a type, for example this could be specified110as::111 112 ItemNeed('update', 27, 'posts')113 ('update', 27, 'posts') # or like this114 115And that might describe the permission to update a particular blog post. In116reality, the developer is free to choose whatever convention the permissions117are.118"""119 120 121class PermissionDenied(RuntimeError):122 """Permission denied to the resource"""123 124 125class Identity(object):126 """Represent the user's identity.127 128 :param id: The user id129 :param auth_type: The authentication type used to confirm the user's130 identity.131 132 The identity is used to represent the user's identity in the system. This133 object is created on login, or on the start of the request as loaded from134 the user's session.135 136 Once loaded it is sent using the `identity-loaded` signal, and should be137 populated with additional required information.138 139 Needs that are provided by this identity should be added to the `provides`140 set after loading.141 """142 def __init__(self, id, auth_type=None):143 self.id = id144 self.auth_type = auth_type145 self.provides = set()146 147 def can(self, permission):148 """Whether the identity has access to the permission.149 150 :param permission: The permission to test provision for.151 """152 return permission.allows(self)153 154 def __repr__(self):155 return '<{0} id="{1}" auth_type="{2}" provides={3}>'.format(156 self.__class__.__name__, self.id, self.auth_type, self.provides157 )158 159 160class AnonymousIdentity(Identity):161 """An anonymous identity"""162 163 def __init__(self):164 Identity.__init__(self, None)165 166 167class IdentityContext(object):168 """The context of an identity for a permission.169 170 .. note:: The principal is usually created by the flaskext.Permission.require method171 call for normal use-cases.172 173 The principal behaves as either a context manager or a decorator. The174 permission is checked for provision in the identity, and if available the175 flow is continued (context manager) or the function is executed (decorator).176 """177 178 def __init__(self, permission, http_exception=None):179 self.permission = permission180 self.http_exception = http_exception181 """The permission of this principal182 """183 184 @property185 def identity(self):186 """The identity of this principal187 """188 return g.identity189 190 def can(self):191 """Whether the identity has access to the permission192 """193 return self.identity.can(self.permission)194 195 def __call__(self, f):196 @wraps(f)197 def _decorated(*args, **kw):198 with self:199 rv = f(*args, **kw)200 return rv201 return _decorated202 203 def __enter__(self):204 # check the permission here205 if not self.can():206 if self.http_exception:207 abort(self.http_exception, self.permission)208 raise PermissionDenied(self.permission)209 210 def __exit__(self, *args):211 return False212 213 214class Permission(object):215 """Represents needs, any of which must be present to access a resource216 217 :param needs: The needs for this permission218 """219 def __init__(self, *needs):220 """A set of needs, any of which must be present in an identity to have221 access.222 """223 224 self.needs = set(needs)225 self.excludes = set()226 227 def _bool(self):228 return bool(self.can())229 230 def __nonzero__(self):231 """Equivalent to ``self.can()``.232 """233 return self._bool()234 235 def __bool__(self):236 """Equivalent to ``self.can()``.237 """238 return self._bool()239 240 def __and__(self, other):241 """Does the same thing as ``self.union(other)``242 """243 return self.union(other)244 245 def __or__(self, other):246 """Does the same thing as ``self.difference(other)``247 """248 return self.difference(other)249 250 def __contains__(self, other):251 """Does the same thing as ``other.issubset(self)``.252 """253 return other.issubset(self)254 255 def __repr__(self):256 return '<{0} needs={1} excludes={2}>'.format(257 self.__class__.__name__, self.needs, self.excludes258 )259 260 def require(self, http_exception=None):261 """Create a principal for this permission.262 263 The principal may be used as a context manager, or a decroator.264 265 If ``http_exception`` is passed then ``abort()`` will be called266 with the HTTP exception code. Otherwise a ``PermissionDenied``267 exception will be raised if the identity does not meet the268 requirements.269 270 :param http_exception: the HTTP exception code (403, 401 etc)271 """272 return IdentityContext(self, http_exception)273 274 def test(self, http_exception=None):275 """276 Checks if permission available and raises relevant exception277 if not. This is useful if you just want to check permission278 without wrapping everything in a require() block.279 280 This is equivalent to::281 282 with permission.require():283 pass284 """285 286 with self.require(http_exception):287 pass288 289 def reverse(self):290 """291 Returns reverse of current state (needs->excludes, excludes->needs)292 """293 294 p = Permission()295 p.needs.update(self.excludes)296 p.excludes.update(self.needs)297 return p298 299 def union(self, other):300 """Create a new permission with the requirements of the union of this301 and other.302 303 :param other: The other permission304 """305 p = Permission(*self.needs.union(other.needs))306 p.excludes.update(self.excludes.union(other.excludes))307 return p308 309 def difference(self, other):310 """Create a new permission consisting of requirements in this311 permission and not in the other.312 """313 314 p = Permission(*self.needs.difference(other.needs))315 p.excludes.update(self.excludes.difference(other.excludes))316 return p317 318 def issubset(self, other):319 """Whether this permission needs are a subset of another320 321 :param other: The other permission322 """323 return (324 self.needs.issubset(other.needs) and325 self.excludes.issubset(other.excludes)326 )327 328 def allows(self, identity):329 """Whether the identity can access this permission.330 331 :param identity: The identity332 """333 if self.needs and not self.needs.intersection(identity.provides):334 return False335 336 if self.excludes and self.excludes.intersection(identity.provides):337 return False338 339 return True340 341 def can(self):342 """Whether the required context for this permission has access343 344 This creates an identity context and tests whether it can access this345 permission346 """347 return self.require().can()348 349 350class Denial(Permission):351 """352 Shortcut class for passing excluded needs.353 """354 355 def __init__(self, *excludes):356 self.excludes = set(excludes)357 self.needs = set()358 359 360def session_identity_loader():361 if 'identity.id' in session and 'identity.auth_type' in session:362 identity = Identity(session['identity.id'],363 session['identity.auth_type'])364 return identity365 366 367def session_identity_saver(identity):368 session['identity.id'] = identity.id369 session['identity.auth_type'] = identity.auth_type370 session.modified = True371 372 373class Principal(object):374 """Principal extension375 376 :param app: The flask application to extend377 :param use_sessions: Whether to use sessions to extract and store378 identification.379 :param skip_static: Whether to ignore static endpoints.380 """381 def __init__(self, app=None, use_sessions=True, skip_static=False):382 self.identity_loaders = deque()383 self.identity_savers = deque()384 # XXX This will probably vanish for a better API385 self.use_sessions = use_sessions386 self.skip_static = skip_static387 388 if app is not None:389 self.init_app(app)390 391 def _init_app(self, app):392 from warnings import warn393 warn(DeprecationWarning(394 '_init_app is deprecated, use the new init_app '395 'method instead.'), stacklevel=1396 )397 self.init_app(app)398 399 def init_app(self, app):400 if hasattr(app, 'static_url_path'):401 self._static_path = app.static_url_path402 else:403 self._static_path = app.static_path404 405 app.before_request(self._on_before_request)406 identity_changed.connect(self._on_identity_changed, app)407 408 if self.use_sessions:409 self.identity_loader(session_identity_loader)410 self.identity_saver(session_identity_saver)411 412 def set_identity(self, identity):413 """Set the current identity.414 415 :param identity: The identity to set416 """417 418 self._set_thread_identity(identity)419 for saver in self.identity_savers:420 saver(identity)421 422 def identity_loader(self, f):423 """Decorator to define a function as an identity loader.424 425 An identity loader function is called before request to find any426 provided identities. The first found identity is used to load from.427 428 For example::429 430 app = Flask(__name__)431 432 principals = Principal(app)433 434 @principals.identity_loader435 def load_identity_from_weird_usecase():436 return Identity('ali')437 """438 self.identity_loaders.appendleft(f)439 return f440 441 def identity_saver(self, f):442 """Decorator to define a function as an identity saver.443 444 An identity loader saver is called when the identity is set to persist445 it for the next request.446 447 For example::448 449 app = Flask(__name__)450 451 principals = Principal(app)452 453 @principals.identity_saver454 def save_identity_to_weird_usecase(identity):455 my_special_cookie['identity'] = identity456 """457 self.identity_savers.appendleft(f)458 return f459 460 def _set_thread_identity(self, identity):461 g.identity = identity462 identity_loaded.send(current_app._get_current_object(),463 identity=identity)464 465 def _on_identity_changed(self, app, identity):466 if self._is_static_route():467 return468 469 self.set_identity(identity)470 471 def _on_before_request(self):472 if self._is_static_route():473 return474 475 g.identity = AnonymousIdentity()476 for loader in self.identity_loaders:477 identity = loader()478 if identity is not None:479 self.set_identity(identity)480 return481 482 def _is_static_route(self):483 return (484 self.skip_static and485 request.path.startswith(self._static_path)486 )487 