Compare commits
2 Commits
master
...
pyup-sched
Author | SHA1 | Date |
---|---|---|
pyup-bot | 140804e4b5 | |
pyup-bot | 6d9f5f03ed |
|
@ -56,4 +56,3 @@ docs/_build/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
.pytest_cache
|
|
13
.travis.yml
13
.travis.yml
|
@ -2,7 +2,7 @@ language: python
|
||||||
python:
|
python:
|
||||||
- 3.5
|
- 3.5
|
||||||
- 3.6
|
- 3.6
|
||||||
- 3.7
|
- 3.7-dev
|
||||||
- nightly
|
- nightly
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -21,13 +21,18 @@ script:
|
||||||
after_success:
|
after_success:
|
||||||
- codecov
|
- codecov
|
||||||
|
|
||||||
|
env:
|
||||||
|
matrix:
|
||||||
|
- PYTHONASYNCIODEBUG=x
|
||||||
|
- PYTHONASYNCIODEBUG=
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: aio-libs-bot
|
user: andrew.svetlov
|
||||||
password:
|
password:
|
||||||
secure: "fHbpT6AuRM+K6hg0nWT7ov/qJAAFJ7N5Mot1Z02QVHv9+XXJsqSmzJHMyv4FNWSO+IG9ulJ/wrVpQ/wr5S1FiCVJYpMFJP/71fqT7MbrgUg+ovbGrs1AfJHHQtVc91Az9Yl2nP+wzJCplJIuxO8IVjKHS87QxupzMHapo97ItYM6yvCNzIP+3JjvZyl5/ocqdUpl4KiS/tzXbiaBSlVgmI/013EbD5U36wcz2AAszTcBzKTDJh0BF4wr4brHnVPKr4gRSZPZRYduZ7WXh0rJt/aGyNGm9siYkKhuE+pzd/6vIbN3keKEhAjafCl4+Z3a0eL0ACyt8CBCBHf9/n4KYm+KPwLe3NYWKkO6qCJpZ+bMNfQInKiEoWJx9KDaKjdCVivlKY+abaJiF/thO4udunn3PfPz2O8MlkZRoTVqASN1sP60cULTlxfLi8x0RVqMKIHejNQi/AN8/4poCPFfFOOia/WQqq1pD45vJh8pNxsc6IEAjhHUgvMDnK0DBkEs4i2catZKc2YPEjgAkvplvTE4tH8Tzyj5EvMwM56h2zfByeKs9ojkvzyhPLhWq7d8JTPWPAyj72FsrpGm12cLU/E9g9KKj6Hg5E3F0+V2Zs7wXc+fT1ovC5/NRL2WpT2+k1wF/9Q7ZrbQ9InunHXYU7GJCFzRE8XlcRsBGPcR3Ls="
|
secure: "JdBvuOBA/198ognVDOY/qZpIKGXfCx47725kyJo/SpQ3nP+x0GLZb3PMQkR0jfSWWkx6Sisk3vOCYsoWclPyPzp+o4ZpfM8yAjHNFmtbr+k+XJdUEApEiWb6/Y3g7DCyY2Qa/L8IYlyABPWrrJI/nld2sKm5kmhFpR/z3HfeFtINP6Ivp34dUOkeRP6kOvCi9d6GyWnvTRnhlybAnk/Ngrroh8XrbKHdDv0zkQkshF8+pmxVzwao4C6S5ld5cFXIYZHLBA9lNC3zgvOMuFeGPUEN9vab3q77MvaiMIuTC9QjcgIhfw3gabH2u7knqfFzqqzXMaVptx5z8o1JtsxMyYt5NVBqS4NPIljpZjaoS/CASHJlRxniJiYfjvjOtFEcfGMNtZj8ZYsGR0nuP2jwzgpEHHWIs4qL0Y8h9t7pGirxCuQcnY10sr+Y+JKaZNJsugNLgbqE2aaZUye5gjDcEj9WY8kKNZXucLP7c0McJuwPqplDEO4CQouMttcKSYkA0QoETmpAFqaXCaMs3p/glOoU2ZyHSH9mXWir69yo84ymb2NlGPMTAstXlv/g/oLmLMSq7lbl6cSUnO1/wxBGlyfv5AAq/75YUaqsgYofzN5CjUgA3m6NedvbWxLUJaxVQ7nduYGEQKDvGEBmzCNv6CdVRCjQ9J1xX3XzkVheQGc="
|
||||||
distributions: "sdist bdist_wheel"
|
distributions: "sdist bdist_wheel"
|
||||||
on:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
all_branches: true
|
all_branches: true
|
||||||
python: 3.7
|
python: 3.6
|
||||||
|
|
23
CHANGES.txt
23
CHANGES.txt
|
@ -1,31 +1,10 @@
|
||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
0.4.0 (2018-09-27)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Bump minimal supported ``aiohttp`` version to 3.2
|
|
||||||
|
|
||||||
- Use ``request.config_dict`` for accessing ``jinja2`` environment. It
|
|
||||||
allows to reuse jinja rendering engine from parent application.
|
|
||||||
|
|
||||||
0.3.0 (2018-09-06)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Deprecate ``login_required`` and ``has_permission`` decorators.
|
|
||||||
Use ``check_authorized`` and ``check_permission`` helper functions instead.
|
|
||||||
|
|
||||||
- Bump supported ``aiohttp`` version to 3.0+
|
|
||||||
|
|
||||||
- Enable strong warnings mode for test suite, clean-up all deprecation
|
|
||||||
warnings.
|
|
||||||
|
|
||||||
- Polish documentation
|
|
||||||
|
|
||||||
0.2.0 (2017-11-17)
|
0.2.0 (2017-11-17)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
- Add ``is_anonymous``, ``login_required``, ``has_permission`` helpers (#114)
|
- Add `is_anonymous`, `login_required`, `has_permission` helpers (#114)
|
||||||
|
|
||||||
0.1.2 (2017-10-17)
|
0.1.2 (2017-10-17)
|
||||||
------------------
|
------------------
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
aiohttp_security
|
aiohttp_security
|
||||||
================
|
================
|
||||||
.. image:: https://travis-ci.com/aio-libs/aiohttp-security.svg?branch=master
|
.. image:: https://travis-ci.org/aio-libs/aiohttp-security.svg?branch=master
|
||||||
:target: https://travis-ci.com/aio-libs/aiohttp-security
|
:target: https://travis-ci.org/aio-libs/aiohttp-security
|
||||||
.. image:: https://codecov.io/github/aio-libs/aiohttp-security/coverage.svg?branch=master
|
.. image:: https://codecov.io/github/aio-libs/aiohttp-security/coverage.svg?branch=master
|
||||||
:target: https://codecov.io/github/aio-libs/aiohttp-security
|
:target: https://codecov.io/github/aio-libs/aiohttp-security
|
||||||
.. image:: https://readthedocs.org/projects/aiohttp-security/badge/?version=latest
|
.. image:: https://readthedocs.org/projects/aiohttp-security/badge/?version=latest
|
||||||
|
@ -46,7 +46,7 @@ https://aiohttp-security.readthedocs.io/
|
||||||
Develop
|
Develop
|
||||||
-------
|
-------
|
||||||
|
|
||||||
``pip install -r requirements-dev.txt``
|
``pip install -r requirements-dev``
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
|
from .abc import AbstractAuthorizationPolicy, AbstractIdentityPolicy
|
||||||
from .api import (authorized_userid, forget, has_permission,
|
from .api import (authorized_userid, forget, has_permission, is_anonymous,
|
||||||
is_anonymous, login_required, permits, remember,
|
login_required, permits, remember, setup)
|
||||||
setup, check_authorized, check_permission)
|
|
||||||
from .cookies_identity import CookiesIdentityPolicy
|
from .cookies_identity import CookiesIdentityPolicy
|
||||||
from .session_identity import SessionIdentityPolicy
|
from .session_identity import SessionIdentityPolicy
|
||||||
from .jwt_identity import JWTIdentityPolicy
|
|
||||||
|
|
||||||
__version__ = '0.4.0'
|
__version__ = '0.2.0'
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
|
__all__ = ('AbstractIdentityPolicy', 'AbstractAuthorizationPolicy',
|
||||||
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
|
'CookiesIdentityPolicy', 'SessionIdentityPolicy',
|
||||||
'JWTIdentityPolicy',
|
|
||||||
'remember', 'forget', 'authorized_userid',
|
'remember', 'forget', 'authorized_userid',
|
||||||
'permits', 'setup', 'is_anonymous',
|
'permits', 'setup', 'is_anonymous',
|
||||||
'login_required', 'has_permission',
|
'login_required', 'has_permission')
|
||||||
'check_authorized', 'check_permission')
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import enum
|
import enum
|
||||||
import warnings
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security.abc import (AbstractIdentityPolicy,
|
from aiohttp_security.abc import (AbstractIdentityPolicy,
|
||||||
AbstractAuthorizationPolicy)
|
AbstractAuthorizationPolicy)
|
||||||
|
@ -19,7 +18,7 @@ async def remember(request, response, identity, **kwargs):
|
||||||
"""
|
"""
|
||||||
assert isinstance(identity, str), identity
|
assert isinstance(identity, str), identity
|
||||||
assert identity
|
assert identity
|
||||||
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
identity_policy = request.app.get(IDENTITY_KEY)
|
||||||
if identity_policy is None:
|
if identity_policy is None:
|
||||||
text = ("Security subsystem is not initialized, "
|
text = ("Security subsystem is not initialized, "
|
||||||
"call aiohttp_security.setup(...) first")
|
"call aiohttp_security.setup(...) first")
|
||||||
|
@ -36,7 +35,7 @@ async def forget(request, response):
|
||||||
Usually it clears cookie or server-side storage to forget user
|
Usually it clears cookie or server-side storage to forget user
|
||||||
session.
|
session.
|
||||||
"""
|
"""
|
||||||
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
identity_policy = request.app.get(IDENTITY_KEY)
|
||||||
if identity_policy is None:
|
if identity_policy is None:
|
||||||
text = ("Security subsystem is not initialized, "
|
text = ("Security subsystem is not initialized, "
|
||||||
"call aiohttp_security.setup(...) first")
|
"call aiohttp_security.setup(...) first")
|
||||||
|
@ -48,8 +47,8 @@ async def forget(request, response):
|
||||||
|
|
||||||
|
|
||||||
async def authorized_userid(request):
|
async def authorized_userid(request):
|
||||||
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
identity_policy = request.app.get(IDENTITY_KEY)
|
||||||
autz_policy = request.config_dict.get(AUTZ_KEY)
|
autz_policy = request.app.get(AUTZ_KEY)
|
||||||
if identity_policy is None or autz_policy is None:
|
if identity_policy is None or autz_policy is None:
|
||||||
return None
|
return None
|
||||||
identity = await identity_policy.identify(request)
|
identity = await identity_policy.identify(request)
|
||||||
|
@ -62,8 +61,8 @@ async def authorized_userid(request):
|
||||||
async def permits(request, permission, context=None):
|
async def permits(request, permission, context=None):
|
||||||
assert isinstance(permission, (str, enum.Enum)), permission
|
assert isinstance(permission, (str, enum.Enum)), permission
|
||||||
assert permission
|
assert permission
|
||||||
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
identity_policy = request.app.get(IDENTITY_KEY)
|
||||||
autz_policy = request.config_dict.get(AUTZ_KEY)
|
autz_policy = request.app.get(AUTZ_KEY)
|
||||||
if identity_policy is None or autz_policy is None:
|
if identity_policy is None or autz_policy is None:
|
||||||
return True
|
return True
|
||||||
identity = await identity_policy.identify(request)
|
identity = await identity_policy.identify(request)
|
||||||
|
@ -78,7 +77,7 @@ async def is_anonymous(request):
|
||||||
User is considered anonymous if there is not identity
|
User is considered anonymous if there is not identity
|
||||||
in request.
|
in request.
|
||||||
"""
|
"""
|
||||||
identity_policy = request.config_dict.get(IDENTITY_KEY)
|
identity_policy = request.app.get(IDENTITY_KEY)
|
||||||
if identity_policy is None:
|
if identity_policy is None:
|
||||||
return True
|
return True
|
||||||
identity = await identity_policy.identify(request)
|
identity = await identity_policy.identify(request)
|
||||||
|
@ -87,15 +86,6 @@ async def is_anonymous(request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def check_authorized(request):
|
|
||||||
"""Checker that raises HTTPUnauthorized for anonymous users.
|
|
||||||
"""
|
|
||||||
userid = await authorized_userid(request)
|
|
||||||
if userid is None:
|
|
||||||
raise web.HTTPUnauthorized()
|
|
||||||
return userid
|
|
||||||
|
|
||||||
|
|
||||||
def login_required(fn):
|
def login_required(fn):
|
||||||
"""Decorator that restrict access only for authorized users.
|
"""Decorator that restrict access only for authorized users.
|
||||||
|
|
||||||
|
@ -111,34 +101,21 @@ def login_required(fn):
|
||||||
"or `def handler(self, request)`.")
|
"or `def handler(self, request)`.")
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
await check_authorized(request)
|
userid = await authorized_userid(request)
|
||||||
return await fn(*args, **kwargs)
|
if userid is None:
|
||||||
|
raise web.HTTPUnauthorized
|
||||||
|
|
||||||
|
ret = await fn(*args, **kwargs)
|
||||||
|
return ret
|
||||||
|
|
||||||
warnings.warn("login_required decorator is deprecated, "
|
|
||||||
"use check_authorized instead",
|
|
||||||
DeprecationWarning)
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
async def check_permission(request, permission, context=None):
|
|
||||||
"""Checker that passes only to authoraised users with given permission.
|
|
||||||
|
|
||||||
If user is not authorized - raises HTTPUnauthorized,
|
|
||||||
if user is authorized and does not have permission -
|
|
||||||
raises HTTPForbidden.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await check_authorized(request)
|
|
||||||
allowed = await permits(request, permission, context)
|
|
||||||
if not allowed:
|
|
||||||
raise web.HTTPForbidden()
|
|
||||||
|
|
||||||
|
|
||||||
def has_permission(
|
def has_permission(
|
||||||
permission,
|
permission,
|
||||||
context=None,
|
context=None,
|
||||||
):
|
):
|
||||||
"""Decorator that restricts access only for authorized users
|
"""Decorator that restrict access only for authorized users
|
||||||
with correct permissions.
|
with correct permissions.
|
||||||
|
|
||||||
If user is not authorized - raises HTTPUnauthorized,
|
If user is not authorized - raises HTTPUnauthorized,
|
||||||
|
@ -155,14 +132,18 @@ def has_permission(
|
||||||
"or `def handler(self, request)`.")
|
"or `def handler(self, request)`.")
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
await check_permission(request, permission, context)
|
userid = await authorized_userid(request)
|
||||||
return await fn(*args, **kwargs)
|
if userid is None:
|
||||||
|
raise web.HTTPUnauthorized
|
||||||
|
|
||||||
|
allowed = await permits(request, permission, context)
|
||||||
|
if not allowed:
|
||||||
|
raise web.HTTPForbidden
|
||||||
|
ret = await fn(*args, **kwargs)
|
||||||
|
return ret
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
warnings.warn("has_permission decorator is deprecated, "
|
|
||||||
"use check_permission instead",
|
|
||||||
DeprecationWarning)
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
"""Identity policy for storing info in the jwt token.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .abc import AbstractIdentityPolicy
|
|
||||||
|
|
||||||
try:
|
|
||||||
import jwt
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
jwt = None
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_HEADER_NAME = 'Authorization'
|
|
||||||
AUTH_SCHEME = 'Bearer '
|
|
||||||
|
|
||||||
|
|
||||||
class JWTIdentityPolicy(AbstractIdentityPolicy):
|
|
||||||
def __init__(self, secret, algorithm='HS256'):
|
|
||||||
if jwt is None:
|
|
||||||
raise RuntimeError('Please install `PyJWT`')
|
|
||||||
self.secret = secret
|
|
||||||
self.algorithm = algorithm
|
|
||||||
|
|
||||||
async def identify(self, request):
|
|
||||||
header_identity = request.headers.get(AUTH_HEADER_NAME)
|
|
||||||
|
|
||||||
if header_identity is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not header_identity.startswith(AUTH_SCHEME):
|
|
||||||
raise ValueError('Invalid authorization scheme. ' +
|
|
||||||
'Should be `Bearer <token>`')
|
|
||||||
|
|
||||||
token = header_identity.split(' ')[1].strip()
|
|
||||||
|
|
||||||
identity = jwt.decode(token,
|
|
||||||
self.secret,
|
|
||||||
algorithms=[self.algorithm])
|
|
||||||
return identity
|
|
||||||
|
|
||||||
async def remember(self, *args, **kwargs): # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def forget(self, request, response): # pragma: no cover
|
|
||||||
pass
|
|
|
@ -11,7 +11,7 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
self.dbengine = dbengine
|
self.dbengine = dbengine
|
||||||
|
|
||||||
async def authorized_userid(self, identity):
|
async def authorized_userid(self, identity):
|
||||||
async with self.dbengine.acquire() as conn:
|
async with self.dbengine as conn:
|
||||||
where = sa.and_(db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.count().where(where)
|
query = db.users.count().where(where)
|
||||||
|
@ -25,7 +25,7 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
if identity is None:
|
if identity is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async with self.dbengine.acquire() as conn:
|
async with self.dbengine as conn:
|
||||||
where = sa.and_(db.users.c.login == identity,
|
where = sa.and_(db.users.c.login == identity,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.select().where(where)
|
query = db.users.select().where(where)
|
||||||
|
@ -50,13 +50,13 @@ class DBAuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
|
|
||||||
async def check_credentials(db_engine, username, password):
|
async def check_credentials(db_engine, username, password):
|
||||||
async with db_engine.acquire() as conn:
|
async with db_engine as conn:
|
||||||
where = sa.and_(db.users.c.login == username,
|
where = sa.and_(db.users.c.login == username,
|
||||||
sa.not_(db.users.c.disabled))
|
sa.not_(db.users.c.disabled))
|
||||||
query = db.users.select().where(where)
|
query = db.users.select().where(where)
|
||||||
ret = await conn.execute(query)
|
ret = await conn.execute(query)
|
||||||
user = await ret.fetchone()
|
user = await ret.fetchone()
|
||||||
if user is not None:
|
if user is not None:
|
||||||
hashed = user[2]
|
hash = user[2]
|
||||||
return sha256_crypt.verify(password, hashed)
|
return sha256_crypt.verify(password, hash)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -4,7 +4,7 @@ from aiohttp import web
|
||||||
|
|
||||||
from aiohttp_security import (
|
from aiohttp_security import (
|
||||||
remember, forget, authorized_userid,
|
remember, forget, authorized_userid,
|
||||||
check_permission, check_authorized,
|
has_permission, login_required,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .db_auth import check_credentials
|
from .db_auth import check_credentials
|
||||||
|
@ -45,25 +45,25 @@ class Web(object):
|
||||||
db_engine = request.app.db_engine
|
db_engine = request.app.db_engine
|
||||||
if await check_credentials(db_engine, login, password):
|
if await check_credentials(db_engine, login, password):
|
||||||
await remember(request, response, login)
|
await remember(request, response, login)
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
raise web.HTTPUnauthorized(
|
return web.HTTPUnauthorized(
|
||||||
body=b'Invalid username/password combination')
|
body=b'Invalid username/password combination')
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def logout(self, request):
|
async def logout(self, request):
|
||||||
await check_authorized(request)
|
|
||||||
response = web.Response(body=b'You have been logged out')
|
response = web.Response(body=b'You have been logged out')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@has_permission('public')
|
||||||
async def internal_page(self, request):
|
async def internal_page(self, request):
|
||||||
await check_permission(request, 'public')
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
body=b'This page is visible for all registered users')
|
body=b'This page is visible for all registered users')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@has_permission('protected')
|
||||||
async def protected_page(self, request):
|
async def protected_page(self, request):
|
||||||
await check_permission(request, 'protected')
|
|
||||||
response = web.Response(body=b'You are on protected page')
|
response = web.Response(body=b'You are on protected page')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ async def init(loop):
|
||||||
password='aiohttp_security',
|
password='aiohttp_security',
|
||||||
database='aiohttp_security',
|
database='aiohttp_security',
|
||||||
host='127.0.0.1')
|
host='127.0.0.1')
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
app.db_engine = db_engine
|
app.db_engine = db_engine
|
||||||
setup_session(app, RedisStorage(redis_pool))
|
setup_session(app, RedisStorage(redis_pool))
|
||||||
setup_security(app,
|
setup_security(app,
|
||||||
|
|
|
@ -4,7 +4,7 @@ from aiohttp import web
|
||||||
|
|
||||||
from aiohttp_security import (
|
from aiohttp_security import (
|
||||||
remember, forget, authorized_userid,
|
remember, forget, authorized_userid,
|
||||||
check_permission, check_authorized,
|
has_permission, login_required,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .authz import check_credentials
|
from .authz import check_credentials
|
||||||
|
@ -55,8 +55,8 @@ async def login(request):
|
||||||
return web.HTTPUnauthorized(body='Invalid username / password combination')
|
return web.HTTPUnauthorized(body='Invalid username / password combination')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
async def logout(request):
|
async def logout(request):
|
||||||
await check_authorized(request)
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text='You have been logged out',
|
text='You have been logged out',
|
||||||
content_type='text/html',
|
content_type='text/html',
|
||||||
|
@ -65,8 +65,9 @@ async def logout(request):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@has_permission('public')
|
||||||
async def internal_page(request):
|
async def internal_page(request):
|
||||||
await check_permission(request, 'public')
|
# pylint: disable=unused-argument
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text='This page is visible for all registered users',
|
text='This page is visible for all registered users',
|
||||||
content_type='text/html',
|
content_type='text/html',
|
||||||
|
@ -74,8 +75,9 @@ async def internal_page(request):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@has_permission('protected')
|
||||||
async def protected_page(request):
|
async def protected_page(request):
|
||||||
await check_permission(request, 'protected')
|
# pylint: disable=unused-argument
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text='You are on protected page',
|
text='You are on protected page',
|
||||||
content_type='text/html',
|
content_type='text/html',
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
from aiohttp import web
|
|
||||||
from aiohttp_session import SimpleCookieStorage, session_middleware
|
|
||||||
from aiohttp_security import check_permission, \
|
|
||||||
is_anonymous, remember, forget, \
|
|
||||||
setup as setup_security, SessionIdentityPolicy
|
|
||||||
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
|
||||||
|
|
||||||
|
|
||||||
# Demo authorization policy for only one user.
|
|
||||||
# User 'jack' has only 'listen' permission.
|
|
||||||
# For more complicated authorization policies see examples
|
|
||||||
# in the 'demo' directory.
|
|
||||||
class SimpleJack_AuthorizationPolicy(AbstractAuthorizationPolicy):
|
|
||||||
async def authorized_userid(self, identity):
|
|
||||||
"""Retrieve authorized user id.
|
|
||||||
Return the user_id of the user identified by the identity
|
|
||||||
or 'None' if no user exists related to the identity.
|
|
||||||
"""
|
|
||||||
if identity == 'jack':
|
|
||||||
return identity
|
|
||||||
|
|
||||||
async def permits(self, identity, permission, context=None):
|
|
||||||
"""Check user permissions.
|
|
||||||
Return True if the identity is allowed the permission
|
|
||||||
in the current context, else return False.
|
|
||||||
"""
|
|
||||||
return identity == 'jack' and permission in ('listen',)
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_root(request):
|
|
||||||
is_logged = not await is_anonymous(request)
|
|
||||||
return web.Response(text='''<html><head></head><body>
|
|
||||||
Hello, I'm Jack, I'm {logged} logged in.<br /><br />
|
|
||||||
<a href="/login">Log me in</a><br />
|
|
||||||
<a href="/logout">Log me out</a><br /><br />
|
|
||||||
Check my permissions,
|
|
||||||
when i'm logged in and logged out.<br />
|
|
||||||
<a href="/listen">Can I listen?</a><br />
|
|
||||||
<a href="/speak">Can I speak?</a><br />
|
|
||||||
</body></html>'''.format(
|
|
||||||
logged='' if is_logged else 'NOT',
|
|
||||||
), content_type='text/html')
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_login_jack(request):
|
|
||||||
redirect_response = web.HTTPFound('/')
|
|
||||||
await remember(request, redirect_response, 'jack')
|
|
||||||
raise redirect_response
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_logout(request):
|
|
||||||
redirect_response = web.HTTPFound('/')
|
|
||||||
await forget(request, redirect_response)
|
|
||||||
raise redirect_response
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_listen(request):
|
|
||||||
await check_permission(request, 'listen')
|
|
||||||
return web.Response(body="I can listen!")
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_speak(request):
|
|
||||||
await check_permission(request, 'speak')
|
|
||||||
return web.Response(body="I can speak!")
|
|
||||||
|
|
||||||
|
|
||||||
async def make_app():
|
|
||||||
#
|
|
||||||
# WARNING!!!
|
|
||||||
# Never use SimpleCookieStorage on production!!!
|
|
||||||
# It’s highly insecure!!!
|
|
||||||
#
|
|
||||||
|
|
||||||
# make app
|
|
||||||
middleware = session_middleware(SimpleCookieStorage())
|
|
||||||
app = web.Application(middlewares=[middleware])
|
|
||||||
|
|
||||||
# add the routes
|
|
||||||
app.router.add_route('GET', '/', handler_root)
|
|
||||||
app.router.add_route('GET', '/login', handler_login_jack)
|
|
||||||
app.router.add_route('GET', '/logout', handler_logout)
|
|
||||||
app.router.add_route('GET', '/listen', handler_listen)
|
|
||||||
app.router.add_route('GET', '/speak', handler_speak)
|
|
||||||
|
|
||||||
# set up policies
|
|
||||||
policy = SessionIdentityPolicy()
|
|
||||||
setup_security(app, policy, SimpleJack_AuthorizationPolicy())
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
web.run_app(make_app(), port=9000)
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
132
docs/example.rst
132
docs/example.rst
|
@ -7,97 +7,63 @@ How to Make a Simple Server With Authorization
|
||||||
|
|
||||||
Simple example::
|
Simple example::
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_session import SimpleCookieStorage, session_middleware
|
|
||||||
from aiohttp_security import check_permission, \
|
async def root_handler(request):
|
||||||
is_anonymous, remember, forget, \
|
text = "Alive and kicking!"
|
||||||
setup as setup_security, SessionIdentityPolicy
|
return web.Response(body=text.encode('utf-8'))
|
||||||
from aiohttp_security.abc import AbstractAuthorizationPolicy
|
|
||||||
|
# option 2: auth at a higher level?
|
||||||
|
# set user_id and allowed in the wsgi handler
|
||||||
|
@protect('view_user')
|
||||||
|
async def user_handler(request):
|
||||||
|
name = request.match_info.get('name', "Anonymous")
|
||||||
|
text = "Hello, " + name
|
||||||
|
return web.Response(body=text.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
# Demo authorization policy for only one user.
|
# option 3: super low
|
||||||
# User 'jack' has only 'listen' permission.
|
# wsgi doesn't do anything
|
||||||
# For more complicated authorization policies see examples
|
async def user_update_handler(request):
|
||||||
# in the 'demo' directory.
|
# identity, asked_permission
|
||||||
class SimpleJack_AuthorizationPolicy(AbstractAuthorizationPolicy):
|
user_id = await identity_policy.identify(request)
|
||||||
async def authorized_userid(self, identity):
|
identity = await auth_policy.authorized_userid(user_id)
|
||||||
"""Retrieve authorized user id.
|
allowed = await request.auth_policy.permits(
|
||||||
Return the user_id of the user identified by the identity
|
identity, asked_permission)
|
||||||
or 'None' if no user exists related to the identity.
|
if not allowed:
|
||||||
"""
|
# how is this pluggable as well?
|
||||||
if identity == 'jack':
|
# ? return NotAllowedStream()
|
||||||
return identity
|
raise NotAllowedResponse()
|
||||||
|
|
||||||
async def permits(self, identity, permission, context=None):
|
update_user()
|
||||||
"""Check user permissions.
|
|
||||||
Return True if the identity is allowed the permission
|
|
||||||
in the current context, else return False.
|
|
||||||
"""
|
|
||||||
return identity == 'jack' and permission in ('listen',)
|
|
||||||
|
|
||||||
|
async def init(loop):
|
||||||
|
# set up identity and auth
|
||||||
|
auth_policy = DictionaryAuthorizationPolicy({'me': ('view_user',),
|
||||||
|
'you': ('view_user',
|
||||||
|
'edit_user',)})
|
||||||
|
identity_policy = CookieIdentityPolicy()
|
||||||
|
auth = authorization_middleware(auth_policy, identity_policy)
|
||||||
|
|
||||||
async def handler_root(request):
|
# wsgi app
|
||||||
is_logged = not await is_anonymous(request)
|
app = web.Application(loop=loop, middlewares=*auth)
|
||||||
return web.Response(text='''<html><head></head><body>
|
|
||||||
Hello, I'm Jack, I'm {logged} logged in.<br /><br />
|
|
||||||
<a href="/login">Log me in</a><br />
|
|
||||||
<a href="/logout">Log me out</a><br /><br />
|
|
||||||
Check my permissions,
|
|
||||||
when i'm logged in and logged out.<br />
|
|
||||||
<a href="/listen">Can I listen?</a><br />
|
|
||||||
<a href="/speak">Can I speak?</a><br />
|
|
||||||
</body></html>'''.format(
|
|
||||||
logged='' if is_logged else 'NOT',
|
|
||||||
), content_type='text/html')
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_login_jack(request):
|
|
||||||
redirect_response = web.HTTPFound('/')
|
|
||||||
await remember(request, redirect_response, 'jack')
|
|
||||||
raise redirect_response
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_logout(request):
|
|
||||||
redirect_response = web.HTTPFound('/')
|
|
||||||
await forget(request, redirect_response)
|
|
||||||
raise redirect_response
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_listen(request):
|
|
||||||
await check_permission(request, 'listen')
|
|
||||||
return web.Response(body="I can listen!")
|
|
||||||
|
|
||||||
|
|
||||||
async def handler_speak(request):
|
|
||||||
await check_permission(request, 'speak')
|
|
||||||
return web.Response(body="I can speak!")
|
|
||||||
|
|
||||||
|
|
||||||
async def make_app():
|
|
||||||
#
|
|
||||||
# WARNING!!!
|
|
||||||
# Never use SimpleCookieStorage on production!!!
|
|
||||||
# It’s highly insecure!!!
|
|
||||||
#
|
|
||||||
|
|
||||||
# make app
|
|
||||||
middleware = session_middleware(SimpleCookieStorage())
|
|
||||||
app = web.Application(middlewares=[middleware])
|
|
||||||
|
|
||||||
# add the routes
|
# add the routes
|
||||||
app.add_routes([
|
app.router.add_route('GET', '/', root_handler)
|
||||||
web.get('/', handler_root),
|
app.router.add_route('GET', '/{user}', user_handler)
|
||||||
web.get('/login', handler_login_jack),
|
app.router.add_route('GET', '/{user}/edit', user_update_handler)
|
||||||
web.get('/logout', handler_logout),
|
|
||||||
web.get('/listen', handler_listen),
|
|
||||||
web.get('/speak', handler_speak)])
|
|
||||||
|
|
||||||
# set up policies
|
# get it started
|
||||||
policy = SessionIdentityPolicy()
|
srv = await loop.create_server(app.make_handler(),
|
||||||
setup_security(app, policy, SimpleJack_AuthorizationPolicy())
|
'127.0.0.1', 8080)
|
||||||
|
print("Server started at http://127.0.0.1:8080")
|
||||||
return app
|
return srv
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
loop = asyncio.get_event_loop()
|
||||||
web.run_app(make_app(), port=9000)
|
loop.run_until_complete(init(loop))
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass # TODO put handler cleanup here
|
||||||
|
|
|
@ -53,7 +53,7 @@ Now you have two tables:
|
||||||
+-----------------+
|
+-----------------+
|
||||||
| user_id |
|
| user_id |
|
||||||
+-----------------+
|
+-----------------+
|
||||||
| perm_name |
|
| permission_name |
|
||||||
+-----------------+
|
+-----------------+
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ Once we have all the code in place we can install it for our application::
|
||||||
password='aiohttp_security',
|
password='aiohttp_security',
|
||||||
database='aiohttp_security',
|
database='aiohttp_security',
|
||||||
host='127.0.0.1')
|
host='127.0.0.1')
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
setup_session(app, RedisStorage(redis_pool))
|
setup_session(app, RedisStorage(redis_pool))
|
||||||
setup_security(app,
|
setup_security(app,
|
||||||
SessionIdentityPolicy(),
|
SessionIdentityPolicy(),
|
||||||
|
@ -142,23 +142,23 @@ Once we have all the code in place we can install it for our application::
|
||||||
|
|
||||||
|
|
||||||
Now we have authorization and can decorate every other view with access rights
|
Now we have authorization and can decorate every other view with access rights
|
||||||
based on permissions. There are already implemented two helpers::
|
based on permissions. There are already implemented two decorators::
|
||||||
|
|
||||||
from aiohttp_security import check_authorized, check_permission
|
from aiohttp_security import has_permission, login_required
|
||||||
|
|
||||||
For each view you need to protect - just apply the decorator on it::
|
For each view you need to protect - just apply the decorator on it::
|
||||||
|
|
||||||
class Web:
|
class Web:
|
||||||
|
@has_permission('protected')
|
||||||
async def protected_page(self, request):
|
async def protected_page(self, request):
|
||||||
await check_permission(request, 'protected')
|
|
||||||
response = web.Response(body=b'You are on protected page')
|
response = web.Response(body=b'You are on protected page')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
or::
|
or::
|
||||||
|
|
||||||
class Web:
|
class Web:
|
||||||
|
@login_required
|
||||||
async def logout(self, request):
|
async def logout(self, request):
|
||||||
await check_authorized(request)
|
|
||||||
response = web.Response(body=b'You have been logged out')
|
response = web.Response(body=b'You have been logged out')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -3,7 +3,6 @@ aiohttp_security
|
||||||
|
|
||||||
The library provides security for :ref:`aiohttp.web<aiohttp-web>`.
|
The library provides security for :ref:`aiohttp.web<aiohttp-web>`.
|
||||||
|
|
||||||
The current version is |version|
|
|
||||||
|
|
||||||
Contents
|
Contents
|
||||||
--------
|
--------
|
||||||
|
|
|
@ -13,19 +13,6 @@
|
||||||
Public API functions
|
Public API functions
|
||||||
====================
|
====================
|
||||||
|
|
||||||
.. function:: setup(app, identity_policy, autz_policy)
|
|
||||||
|
|
||||||
Setup :mod:`aiohttp` application with security policies.
|
|
||||||
|
|
||||||
:param app: aiohttp :class:`aiohttp.web.Application` instance.
|
|
||||||
|
|
||||||
:param identity_policy: indentification policy, an
|
|
||||||
:class:`AbstractIdentityPolicy` instance.
|
|
||||||
|
|
||||||
:param autz_policy: authorization policy, an
|
|
||||||
:class:`AbstractAuthorizationPolicy` instance.
|
|
||||||
|
|
||||||
|
|
||||||
.. coroutinefunction:: remember(request, response, identity, **kwargs)
|
.. coroutinefunction:: remember(request, response, identity, **kwargs)
|
||||||
|
|
||||||
Remember *identity* in *response*, e.g. by storing a cookie or
|
Remember *identity* in *response*, e.g. by storing a cookie or
|
||||||
|
@ -63,41 +50,6 @@ Public API functions
|
||||||
descendants like :class:`aiohttp.web.Response`.
|
descendants like :class:`aiohttp.web.Response`.
|
||||||
|
|
||||||
|
|
||||||
.. coroutinefunction:: check_authorized(request)
|
|
||||||
|
|
||||||
Checker that doesn't pass if user is not authorized by *request*.
|
|
||||||
|
|
||||||
:param request: :class:`aiohttp.web.Request` object.
|
|
||||||
|
|
||||||
:return str: authorized user ID if success
|
|
||||||
|
|
||||||
:raise: :class:`aiohttp.web.HTTPUnauthorized` for anonymous users.
|
|
||||||
|
|
||||||
Usage::
|
|
||||||
|
|
||||||
async def handler(request):
|
|
||||||
await check_authorized(request)
|
|
||||||
# this line is never executed for anonymous users
|
|
||||||
|
|
||||||
|
|
||||||
.. coroutinefunction:: check_permission(request, permission)
|
|
||||||
|
|
||||||
Checker that doesn't pass if user has no requested permission.
|
|
||||||
|
|
||||||
:param request: :class:`aiohttp.web.Request` object.
|
|
||||||
|
|
||||||
:raise: :class:`aiohttp.web.HTTPUnauthorized` for anonymous users.
|
|
||||||
|
|
||||||
:raise: :class:`aiohttp.web.HTTPForbidden` if user is
|
|
||||||
authorized but has no access rights.
|
|
||||||
|
|
||||||
Usage::
|
|
||||||
|
|
||||||
async def handler(request):
|
|
||||||
await check_permission(request, 'read')
|
|
||||||
# this line is never executed if a user has no read permission
|
|
||||||
|
|
||||||
|
|
||||||
.. coroutinefunction:: authorized_userid(request)
|
.. coroutinefunction:: authorized_userid(request)
|
||||||
|
|
||||||
Retrieve :term:`userid`.
|
Retrieve :term:`userid`.
|
||||||
|
@ -126,8 +78,7 @@ Public API functions
|
||||||
|
|
||||||
:param request: :class:`aiohttp.web.Request` object.
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
|
|
||||||
:param permission: Requested :term:`permission`. :class:`str` or
|
:param permission: Requested :term:`permission`. :class:`str` or :class:`enum.Enum` object.
|
||||||
:class:`enum.Enum` object.
|
|
||||||
|
|
||||||
:param context: additional object may be passed into
|
:param context: additional object may be passed into
|
||||||
:meth:`AbstractAuthorizationPolicy.permission`
|
:meth:`AbstractAuthorizationPolicy.permission`
|
||||||
|
@ -141,8 +92,7 @@ Public API functions
|
||||||
|
|
||||||
Checks if user is anonymous user.
|
Checks if user is anonymous user.
|
||||||
|
|
||||||
Return ``True`` if user is not remembered in request, otherwise
|
Return ``True`` if user is not remembered in request, otherwise returns ``False``.
|
||||||
returns ``False``.
|
|
||||||
|
|
||||||
:param request: :class:`aiohttp.web.Request` object.
|
:param request: :class:`aiohttp.web.Request` object.
|
||||||
|
|
||||||
|
@ -153,27 +103,29 @@ Public API functions
|
||||||
|
|
||||||
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
|
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
|
||||||
|
|
||||||
.. deprecated:: 0.3
|
|
||||||
|
|
||||||
Use :func:`check_authorized` async function.
|
|
||||||
|
|
||||||
|
|
||||||
.. decorator:: has_permission(permission)
|
.. decorator:: has_permission(permission)
|
||||||
|
|
||||||
Decorator for handlers that checks if user is authorized
|
Decorator for handlers that checks if user is authorized
|
||||||
and has correct permission.
|
and has correct permission.
|
||||||
|
|
||||||
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not
|
Raises :class:`aiohttp.web.HTTPUnauthorized` if user is not authorized.
|
||||||
authorized.
|
Raises :class:`aiohttp.web.HTTPForbidden` if user is authorized but has no access rights.
|
||||||
|
|
||||||
Raises :class:`aiohttp.web.HTTPForbidden` if user is
|
|
||||||
authorized but has no access rights.
|
|
||||||
|
|
||||||
:param str permission: requested :term:`permission`.
|
:param str permission: requested :term:`permission`.
|
||||||
|
|
||||||
.. deprecated:: 0.3
|
|
||||||
|
|
||||||
Use :func:`check_authorized` async function.
|
.. function:: setup(app, identity_policy, autz_policy)
|
||||||
|
|
||||||
|
Setup :mod:`aiohttp` application with security policies.
|
||||||
|
|
||||||
|
:param app: aiohttp :class:`aiohttp.web.Application` instance.
|
||||||
|
|
||||||
|
:param identity_policy: indentification policy, an
|
||||||
|
:class:`AbstractIdentityPolicy` instance.
|
||||||
|
|
||||||
|
:param autz_policy: authorization policy, an
|
||||||
|
:class:`AbstractAuthorizationPolicy` instance.
|
||||||
|
|
||||||
|
|
||||||
Abstract policies
|
Abstract policies
|
||||||
|
|
135
docs/usage.rst
135
docs/usage.rst
|
@ -11,132 +11,39 @@
|
||||||
|
|
||||||
First of all, what is *aiohttp_security* about?
|
First of all, what is *aiohttp_security* about?
|
||||||
|
|
||||||
*aiohttp-security* is a set of public API functions as well as a
|
It is a set of public API functions and standard for implementation details.
|
||||||
reference standard for implementation details for securing access to
|
|
||||||
assets served by a wsgi server.
|
|
||||||
|
|
||||||
Assets are secured using authentication and authorization as explained
|
|
||||||
below. *aiohttp-security* is part of the
|
|
||||||
`aio-libs <https://github.com/aio-libs>`_ project which takes advantage
|
|
||||||
of asynchronous processing using Python's asyncio library.
|
|
||||||
|
|
||||||
|
|
||||||
Public API
|
Public API
|
||||||
==========
|
==========
|
||||||
|
|
||||||
The API is agnostic to the low level implementation details such that
|
API is implementation agnostic, all client code should not call policy
|
||||||
all client code only needs to implement the endpoints as provided by
|
code (see below) directly but use API only.
|
||||||
the API (instead of calling policy code directly (see explanation
|
|
||||||
below)).
|
|
||||||
|
|
||||||
Via the API an application can:
|
|
||||||
|
|
||||||
(i) remember a user in a local session (:func:`remember`),
|
|
||||||
(ii) forget a user in a local session (:func:`forget`),
|
|
||||||
(iii) retrieve the :term:`userid` (:func:`authorized_userid`) of a
|
|
||||||
remembered user from an :term:`identity` (discussed below), and
|
|
||||||
(iv) check the :term:`permission` of a remembered user (:func:`permits`).
|
|
||||||
|
|
||||||
The library internals are built on top of two concepts:
|
|
||||||
|
|
||||||
1) :term:`authentication`, and
|
|
||||||
2) :term:`authorization`.
|
|
||||||
|
|
||||||
There are abstract base classes for both types as well as several
|
|
||||||
pre-built implementations that are shipped with the library. However,
|
|
||||||
the end user is free to build their own implementations.
|
|
||||||
|
|
||||||
The library comes with two pre-built identity policies; one that uses
|
|
||||||
cookies, and one that uses sessions [#f1]_. It is envisioned that in
|
|
||||||
most use cases developers will use one of the provided identity
|
|
||||||
policies (Cookie or Session) and implement their own authorization
|
|
||||||
policy.
|
|
||||||
|
|
||||||
The workflow is as follows:
|
|
||||||
|
|
||||||
1) User is authenticated. This has to be implemented by the developer.
|
|
||||||
2) Once user is authenticated an identity string has to be created for
|
|
||||||
that user. This has to be implemented by the developer.
|
|
||||||
3) The identity string is passed to the Identity Policy's remember
|
|
||||||
method and the user is now remembered (Cookie or Session if using
|
|
||||||
built-in). *Only once a user is remembered can the other API
|
|
||||||
methods:* :func:`permits`, :func:`forget`, *and*
|
|
||||||
:func:`authorized_userid` *be invoked* .
|
|
||||||
4) If the user tries to access a restricted asset the :func:`permits`
|
|
||||||
method is called. Usually assets are protected using the
|
|
||||||
:func:`check_permission` helper. This should return True if
|
|
||||||
permission is granted.
|
|
||||||
|
|
||||||
The :func:`permits` method is implemented by the developer as part of
|
|
||||||
the :class:`AbstractAuthorizationPolicy` and passed to the
|
|
||||||
application at runtime via setup.
|
|
||||||
|
|
||||||
In addition a :func:`check_authorized` also
|
|
||||||
exists that requires no permissions (i.e. doesn't call :func:`permits`
|
|
||||||
method) but only requires that the user is remembered
|
|
||||||
(i.e. authenticated/logged in).
|
|
||||||
|
|
||||||
|
Via API application can remember/forget user in local session
|
||||||
|
(:func:`remember`/:func:`forget`), retrieve :term:`userid`
|
||||||
|
(:func:`authorized_userid`) and check :term:`permission` for
|
||||||
|
remembered user (:func:`permits`).
|
||||||
|
|
||||||
|
The library internals are built on top of two policies:
|
||||||
|
:term:`authentication` and :term:`authorization`. There are abstract
|
||||||
|
base classes for both concepts as well as several implementations
|
||||||
|
shipped with the library. End user is free to build own implemetations
|
||||||
|
if needed.
|
||||||
|
|
||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Authentication is the process where a user's identity is verified. It
|
Actions related to retrieving, storing and removing user's
|
||||||
confirms who the user is. This is traditionally done using a user name
|
:term:`identity`.
|
||||||
and password (note: this is not the only way).
|
|
||||||
|
|
||||||
A authenticated user has no access rights, rather an authenticated
|
Authenticated user has no access rights, the system even has no
|
||||||
user merely confirms that the user exists and that the user is who
|
knowledge is there the user still registered in DB.
|
||||||
they say they are.
|
|
||||||
|
|
||||||
In *aiohttp_security* the developer is responsible for their own
|
If :class:`aiohttp.web.Request` has an :term:`identity` it means the user has
|
||||||
authentication mechanism. *aiohttp_security* only requires that the
|
some ID that should be checked by :term:`authorization` policy.
|
||||||
authentication result in a identity string which corresponds to a
|
|
||||||
user's id in the underlying system.
|
|
||||||
|
|
||||||
.. note::
|
:term:`identity` is a string shared between browser and server.
|
||||||
|
Thus it's not supposed to be database primary key, user login/email etc.
|
||||||
:term:`identity` is a string that is shared between the browser and
|
Random string like uuid or hash is better choice.
|
||||||
the server. Therefore it is recommended that a random string
|
|
||||||
such as a uuid or hash is used rather than things like a
|
|
||||||
database primary key, user login/email, etc.
|
|
||||||
|
|
||||||
Identity Policy
|
|
||||||
===============
|
|
||||||
|
|
||||||
Once a user is authenticated the *aiohttp_security* API is invoked for
|
|
||||||
storing, retrieving, and removing a user's :term:`identity`. This is
|
|
||||||
accommplished via AbstractIdentityPolicy's :func:`remember`,
|
|
||||||
:func:`identify`, and :func:`forget` methods. The Identity Policy is
|
|
||||||
therefore the mechanism by which a authenticated user is persisted in
|
|
||||||
the system.
|
|
||||||
|
|
||||||
*aiohttp_security* has two built in identity policy's for this
|
|
||||||
purpose. :class:`CookiesIdentityPolicy` that uses cookies and
|
|
||||||
:class:`SessionIdentityPolicy` that uses sessions via
|
|
||||||
``aiohttp-session`` library.
|
|
||||||
|
|
||||||
Authorization
|
|
||||||
==============
|
|
||||||
|
|
||||||
Once a user is authenticated (see above) it means that the user has an
|
|
||||||
:term:`identity`. This :term:`identity` can now be used for checking
|
|
||||||
access rights or :term:`permission` using a :term:`authorization`
|
|
||||||
policy.
|
|
||||||
|
|
||||||
The authorization policy's :func:`permits()` method is used for this purpose.
|
|
||||||
|
|
||||||
|
|
||||||
When :class:`aiohttp.web.Request` has an :term:`identity` it means the
|
|
||||||
user has been authenticated and therefore has an :term:`identity` that
|
|
||||||
can be checked by the :term:`authorization` policy.
|
|
||||||
|
|
||||||
As noted above, :term:`identity` is a string that is shared between
|
|
||||||
the browser and the server. Therefore it is recommended that a
|
|
||||||
random string such as a uuid or hash is used rather than things like
|
|
||||||
a database primary key, user login/email, etc.
|
|
||||||
|
|
||||||
|
|
||||||
.. rubric:: Footnotes
|
|
||||||
.. [#f1] jwt - json web tokens in the works
|
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
-e .
|
-e .
|
||||||
flake8==3.8.4
|
flake8==3.5.0
|
||||||
async-timeout==3.0.1
|
pytest==3.4.2
|
||||||
pytest==6.2.1
|
pytest-cov==2.5.1
|
||||||
pytest-cov==2.10.1
|
coverage==4.5.1
|
||||||
pytest-mock==3.4.0
|
sphinx==1.7.1
|
||||||
coverage==5.3.1
|
|
||||||
sphinx==3.4.1
|
|
||||||
pep257==0.7.0
|
pep257==0.7.0
|
||||||
aiohttp-session==2.9.0
|
aiohttp-session==2.3.0
|
||||||
aiopg[sa]==1.1.0
|
aiopg[sa]==0.13.2
|
||||||
aioredis==1.3.1
|
aioredis==1.1.0
|
||||||
hiredis==1.1.0
|
hiredis==0.2.0
|
||||||
passlib==1.7.4
|
passlib==1.7.1
|
||||||
cryptography==3.3.1
|
cryptography==2.2
|
||||||
aiohttp==3.7.3
|
aiohttp==3.0.9
|
||||||
pytest-aiohttp==0.3.0
|
pytest-aiohttp==0.3.0
|
||||||
pyjwt==1.7.1
|
|
||||||
|
|
8
setup.py
8
setup.py
|
@ -1,3 +1,4 @@
|
||||||
|
import codecs
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -15,8 +16,8 @@ class PyTest(TestCommand):
|
||||||
raise SystemExit(errno)
|
raise SystemExit(errno)
|
||||||
|
|
||||||
|
|
||||||
with open(os.path.join(os.path.abspath(os.path.dirname(
|
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
||||||
__file__)), 'aiohttp_security', '__init__.py'), 'r', encoding='latin1') as fp:
|
__file__)), 'aiohttp_security', '__init__.py'), 'r', 'latin1') as fp:
|
||||||
try:
|
try:
|
||||||
version = re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0]
|
version = re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
@ -27,7 +28,7 @@ def read(f):
|
||||||
return open(os.path.join(os.path.dirname(__file__), f)).read().strip()
|
return open(os.path.join(os.path.dirname(__file__), f)).read().strip()
|
||||||
|
|
||||||
|
|
||||||
install_requires = ['aiohttp>=3.2.0']
|
install_requires = ['aiohttp>=0.18']
|
||||||
tests_require = install_requires + ['pytest']
|
tests_require = install_requires + ['pytest']
|
||||||
extras_require = {'session': 'aiohttp-session'}
|
extras_require = {'session': 'aiohttp-session'}
|
||||||
|
|
||||||
|
@ -43,7 +44,6 @@ setup(name='aiohttp-security',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Programming Language :: Python :: 3.7',
|
|
||||||
'Topic :: Internet :: WWW/HTTP',
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
'Framework :: AsyncIO',
|
'Framework :: AsyncIO',
|
||||||
],
|
],
|
||||||
|
|
|
@ -15,23 +15,23 @@ class Autz(AbstractAuthorizationPolicy):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def test_remember(loop, aiohttp_client):
|
async def test_remember(loop, test_client):
|
||||||
|
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
await remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', handler)
|
app.router.add_route('GET', '/', handler)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert 'Andrew' == resp.cookies['AIOHTTP_SECURITY'].value
|
assert 'Andrew' == resp.cookies['AIOHTTP_SECURITY'].value
|
||||||
|
|
||||||
|
|
||||||
async def test_identify(loop, aiohttp_client):
|
async def test_identify(loop, test_client):
|
||||||
|
|
||||||
async def create(request):
|
async def create(request):
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
|
@ -44,11 +44,11 @@ async def test_identify(loop, aiohttp_client):
|
||||||
assert 'Andrew' == user_id
|
assert 'Andrew' == user_id
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/', create)
|
app.router.add_route('POST', '/', create)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.post('/')
|
resp = await client.post('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
await resp.release()
|
await resp.release()
|
||||||
|
@ -56,7 +56,7 @@ async def test_identify(loop, aiohttp_client):
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_forget(loop, aiohttp_client):
|
async def test_forget(loop, test_client):
|
||||||
|
|
||||||
async def index(request):
|
async def index(request):
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
@ -64,19 +64,19 @@ async def test_forget(loop, aiohttp_client):
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def logout(request):
|
async def logout(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
assert str(resp.url).endswith('/')
|
assert str(resp.url).endswith('/')
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import enum
|
import enum
|
||||||
import pytest
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_security import setup as _setup
|
from aiohttp_security import setup as _setup
|
||||||
from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid,
|
from aiohttp_security import (AbstractAuthorizationPolicy, authorized_userid,
|
||||||
forget, has_permission, is_anonymous,
|
forget, has_permission, is_anonymous,
|
||||||
login_required, permits, remember,
|
login_required, permits, remember)
|
||||||
check_authorized, check_permission)
|
|
||||||
from aiohttp_security.cookies_identity import CookiesIdentityPolicy
|
from aiohttp_security.cookies_identity import CookiesIdentityPolicy
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,23 +23,23 @@ class Autz(AbstractAuthorizationPolicy):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def test_authorized_userid(loop, aiohttp_client):
|
async def test_authorized_userid(loop, test_client):
|
||||||
|
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def check(request):
|
async def check(request):
|
||||||
userid = await authorized_userid(request)
|
userid = await authorized_userid(request)
|
||||||
assert 'Andrew' == userid
|
assert 'Andrew' == userid
|
||||||
return web.Response(text=userid)
|
return web.Response(text=userid)
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
|
|
||||||
resp = await client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
@ -49,23 +47,23 @@ async def test_authorized_userid(loop, aiohttp_client):
|
||||||
assert 'Andrew' == txt
|
assert 'Andrew' == txt
|
||||||
|
|
||||||
|
|
||||||
async def test_authorized_userid_not_authorized(loop, aiohttp_client):
|
async def test_authorized_userid_not_authorized(loop, test_client):
|
||||||
|
|
||||||
async def check(request):
|
async def check(request):
|
||||||
userid = await authorized_userid(request)
|
userid = await authorized_userid(request)
|
||||||
assert userid is None
|
assert userid is None
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
|
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_permits_enum_permission(loop, aiohttp_client):
|
async def test_permits_enum_permission(loop, test_client):
|
||||||
class Permission(enum.Enum):
|
class Permission(enum.Enum):
|
||||||
READ = '101'
|
READ = '101'
|
||||||
WRITE = '102'
|
WRITE = '102'
|
||||||
|
@ -88,7 +86,7 @@ async def test_permits_enum_permission(loop, aiohttp_client):
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def check(request):
|
async def check(request):
|
||||||
ret = await permits(request, Permission.READ)
|
ret = await permits(request, Permission.READ)
|
||||||
|
@ -99,16 +97,16 @@ async def test_permits_enum_permission(loop, aiohttp_client):
|
||||||
assert not ret
|
assert not ret
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_permits_unauthorized(loop, aiohttp_client):
|
async def test_permits_unauthorized(loop, test_client):
|
||||||
|
|
||||||
async def check(request):
|
async def check(request):
|
||||||
ret = await permits(request, 'read')
|
ret = await permits(request, 'read')
|
||||||
|
@ -119,38 +117,38 @@ async def test_permits_unauthorized(loop, aiohttp_client):
|
||||||
assert not ret
|
assert not ret
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_is_anonymous(loop, aiohttp_client):
|
async def test_is_anonymous(loop, test_client):
|
||||||
|
|
||||||
async def index(request):
|
async def index(request):
|
||||||
is_anon = await is_anonymous(request)
|
is_anon = await is_anonymous(request)
|
||||||
if is_anon:
|
if is_anon:
|
||||||
raise web.HTTPUnauthorized()
|
return web.HTTPUnauthorized()
|
||||||
return web.Response()
|
return web.HTTPOk()
|
||||||
|
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def logout(request):
|
async def logout(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
@ -163,63 +161,27 @@ async def test_is_anonymous(loop, aiohttp_client):
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_login_required(loop, aiohttp_client):
|
async def test_login_required(loop, test_client):
|
||||||
with pytest.raises(DeprecationWarning):
|
@login_required
|
||||||
|
|
||||||
@login_required
|
|
||||||
async def index(request):
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
async def login(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
|
||||||
await remember(request, response, 'UserID')
|
|
||||||
raise response
|
|
||||||
|
|
||||||
async def logout(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
|
||||||
await forget(request, response)
|
|
||||||
raise response
|
|
||||||
|
|
||||||
app = web.Application()
|
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
|
||||||
app.router.add_route('GET', '/', index)
|
|
||||||
app.router.add_route('POST', '/login', login)
|
|
||||||
app.router.add_route('POST', '/logout', logout)
|
|
||||||
|
|
||||||
client = await aiohttp_client(app)
|
|
||||||
resp = await client.get('/')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
|
|
||||||
await client.post('/login')
|
|
||||||
resp = await client.get('/')
|
|
||||||
assert web.HTTPOk.status_code == resp.status
|
|
||||||
|
|
||||||
await client.post('/logout')
|
|
||||||
resp = await client.get('/')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
|
|
||||||
|
|
||||||
async def test_check_authorized(loop, aiohttp_client):
|
|
||||||
async def index(request):
|
async def index(request):
|
||||||
await check_authorized(request)
|
return web.HTTPOk()
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def logout(request):
|
async def logout(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
@ -232,97 +194,38 @@ async def test_check_authorized(loop, aiohttp_client):
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_has_permission(loop, aiohttp_client):
|
async def test_has_permission(loop, test_client):
|
||||||
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
|
|
||||||
@has_permission('read')
|
|
||||||
async def index_read(request):
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
@has_permission('write')
|
|
||||||
async def index_write(request):
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
@has_permission('forbid')
|
|
||||||
async def index_forbid(request):
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
async def login(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
|
||||||
await remember(request, response, 'UserID')
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def logout(request):
|
|
||||||
response = web.HTTPFound(location='/')
|
|
||||||
await forget(request, response)
|
|
||||||
raise response
|
|
||||||
|
|
||||||
app = web.Application()
|
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
|
||||||
app.router.add_route('GET', '/permission/read', index_read)
|
|
||||||
app.router.add_route('GET', '/permission/write', index_write)
|
|
||||||
app.router.add_route('GET', '/permission/forbid', index_forbid)
|
|
||||||
app.router.add_route('POST', '/login', login)
|
|
||||||
app.router.add_route('POST', '/logout', logout)
|
|
||||||
client = await aiohttp_client(app)
|
|
||||||
|
|
||||||
resp = await client.get('/permission/read')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
resp = await client.get('/permission/write')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
resp = await client.get('/permission/forbid')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
|
|
||||||
await client.post('/login')
|
|
||||||
resp = await client.get('/permission/read')
|
|
||||||
assert web.HTTPOk.status_code == resp.status
|
|
||||||
resp = await client.get('/permission/write')
|
|
||||||
assert web.HTTPOk.status_code == resp.status
|
|
||||||
resp = await client.get('/permission/forbid')
|
|
||||||
assert web.HTTPForbidden.status_code == resp.status
|
|
||||||
|
|
||||||
await client.post('/logout')
|
|
||||||
resp = await client.get('/permission/read')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
resp = await client.get('/permission/write')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
resp = await client.get('/permission/forbid')
|
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
|
||||||
|
|
||||||
|
|
||||||
async def test_check_permission(loop, aiohttp_client):
|
|
||||||
|
|
||||||
|
@has_permission('read')
|
||||||
async def index_read(request):
|
async def index_read(request):
|
||||||
await check_permission(request, 'read')
|
return web.HTTPOk()
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
|
@has_permission('write')
|
||||||
async def index_write(request):
|
async def index_write(request):
|
||||||
await check_permission(request, 'write')
|
return web.HTTPOk()
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
|
@has_permission('forbid')
|
||||||
async def index_forbid(request):
|
async def index_forbid(request):
|
||||||
await check_permission(request, 'forbid')
|
return web.HTTPOk()
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'UserID')
|
await remember(request, response, 'UserID')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def logout(request):
|
async def logout(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
_setup(app, CookiesIdentityPolicy(), Autz())
|
_setup(app, CookiesIdentityPolicy(), Autz())
|
||||||
app.router.add_route('GET', '/permission/read', index_read)
|
app.router.add_route('GET', '/permission/read', index_read)
|
||||||
app.router.add_route('GET', '/permission/write', index_write)
|
app.router.add_route('GET', '/permission/write', index_write)
|
||||||
app.router.add_route('GET', '/permission/forbid', index_forbid)
|
app.router.add_route('GET', '/permission/forbid', index_forbid)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
|
|
||||||
resp = await client.get('/permission/read')
|
resp = await client.get('/permission/read')
|
||||||
assert web.HTTPUnauthorized.status_code == resp.status
|
assert web.HTTPUnauthorized.status_code == resp.status
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
import jwt
|
|
||||||
import pytest
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from aiohttp_security import setup as _setup
|
|
||||||
from aiohttp_security import AbstractAuthorizationPolicy
|
|
||||||
from aiohttp_security.api import IDENTITY_KEY
|
|
||||||
from aiohttp_security.jwt_identity import JWTIdentityPolicy
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def make_token():
|
|
||||||
def factory(payload, secret):
|
|
||||||
return jwt.encode(
|
|
||||||
payload,
|
|
||||||
secret,
|
|
||||||
algorithm='HS256',
|
|
||||||
)
|
|
||||||
|
|
||||||
return factory
|
|
||||||
|
|
||||||
|
|
||||||
class Autz(AbstractAuthorizationPolicy):
|
|
||||||
|
|
||||||
async def permits(self, identity, permission, context=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def authorized_userid(self, identity):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def test_no_pyjwt_installed(mocker):
|
|
||||||
mocker.patch('aiohttp_security.jwt_identity.jwt', None)
|
|
||||||
with pytest.raises(RuntimeError):
|
|
||||||
JWTIdentityPolicy('secret')
|
|
||||||
|
|
||||||
|
|
||||||
async def test_identify(loop, make_token, aiohttp_client):
|
|
||||||
kwt_secret_key = 'Key'
|
|
||||||
|
|
||||||
token = make_token({'login': 'Andrew'}, kwt_secret_key)
|
|
||||||
|
|
||||||
async def check(request):
|
|
||||||
policy = request.app[IDENTITY_KEY]
|
|
||||||
identity = await policy.identify(request)
|
|
||||||
assert 'Andrew' == identity['login']
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
app = web.Application()
|
|
||||||
_setup(app, JWTIdentityPolicy(kwt_secret_key), Autz())
|
|
||||||
app.router.add_route('GET', '/', check)
|
|
||||||
|
|
||||||
client = await aiohttp_client(app)
|
|
||||||
headers = {'Authorization': 'Bearer {}'.format(token.decode('utf-8'))}
|
|
||||||
resp = await client.get('/', headers=headers)
|
|
||||||
assert 200 == resp.status
|
|
||||||
|
|
||||||
|
|
||||||
async def test_identify_broken_scheme(loop, make_token, aiohttp_client):
|
|
||||||
kwt_secret_key = 'Key'
|
|
||||||
|
|
||||||
token = make_token({'login': 'Andrew'}, kwt_secret_key)
|
|
||||||
|
|
||||||
async def check(request):
|
|
||||||
policy = request.app[IDENTITY_KEY]
|
|
||||||
|
|
||||||
try:
|
|
||||||
await policy.identify(request)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise web.HTTPBadRequest(reason=exc)
|
|
||||||
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
app = web.Application()
|
|
||||||
_setup(app, JWTIdentityPolicy(kwt_secret_key), Autz())
|
|
||||||
app.router.add_route('GET', '/', check)
|
|
||||||
|
|
||||||
client = await aiohttp_client(app)
|
|
||||||
headers = {'Authorization': 'Token {}'.format(token.decode('utf-8'))}
|
|
||||||
resp = await client.get('/', headers=headers)
|
|
||||||
assert 400 == resp.status
|
|
||||||
assert 'Invalid authorization scheme' in resp.reason
|
|
|
@ -2,21 +2,21 @@ from aiohttp import web
|
||||||
from aiohttp_security import authorized_userid, permits
|
from aiohttp_security import authorized_userid, permits
|
||||||
|
|
||||||
|
|
||||||
async def test_authorized_userid(loop, aiohttp_client):
|
async def test_authorized_userid(loop, test_client):
|
||||||
|
|
||||||
async def check(request):
|
async def check(request):
|
||||||
userid = await authorized_userid(request)
|
userid = await authorized_userid(request)
|
||||||
assert userid is None
|
assert userid is None
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_permits(loop, aiohttp_client):
|
async def test_permits(loop, test_client):
|
||||||
|
|
||||||
async def check(request):
|
async def check(request):
|
||||||
ret = await permits(request, 'read')
|
ret = await permits(request, 'read')
|
||||||
|
@ -27,8 +27,8 @@ async def test_permits(loop, aiohttp_client):
|
||||||
assert ret
|
assert ret
|
||||||
return web.Response()
|
return web.Response()
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
|
@ -2,15 +2,15 @@ from aiohttp import web
|
||||||
from aiohttp_security import remember, forget
|
from aiohttp_security import remember, forget
|
||||||
|
|
||||||
|
|
||||||
async def test_remember(loop, aiohttp_client):
|
async def test_remember(loop, test_client):
|
||||||
|
|
||||||
async def do_remember(request):
|
async def do_remember(request):
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
await remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
app.router.add_route('POST', '/', do_remember)
|
app.router.add_route('POST', '/', do_remember)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.post('/')
|
resp = await client.post('/')
|
||||||
assert 500 == resp.status
|
assert 500 == resp.status
|
||||||
assert (('Security subsystem is not initialized, '
|
assert (('Security subsystem is not initialized, '
|
||||||
|
@ -18,15 +18,15 @@ async def test_remember(loop, aiohttp_client):
|
||||||
resp.reason)
|
resp.reason)
|
||||||
|
|
||||||
|
|
||||||
async def test_forget(loop, aiohttp_client):
|
async def test_forget(loop, test_client):
|
||||||
|
|
||||||
async def do_forget(request):
|
async def do_forget(request):
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
app.router.add_route('POST', '/', do_forget)
|
app.router.add_route('POST', '/', do_forget)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.post('/')
|
resp = await client.post('/')
|
||||||
assert 500 == resp.status
|
assert 500 == resp.status
|
||||||
assert (('Security subsystem is not initialized, '
|
assert (('Security subsystem is not initialized, '
|
||||||
|
|
|
@ -20,14 +20,14 @@ class Autz(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def make_app():
|
def make_app(loop):
|
||||||
app = web.Application()
|
app = web.Application(loop=loop)
|
||||||
setup_session(app, SimpleCookieStorage())
|
setup_session(app, SimpleCookieStorage())
|
||||||
setup_security(app, SessionIdentityPolicy(), Autz())
|
setup_security(app, SessionIdentityPolicy(), Autz())
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
async def test_remember(make_app, aiohttp_client):
|
async def test_remember(make_app, test_client):
|
||||||
|
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
|
@ -37,12 +37,12 @@ async def test_remember(make_app, aiohttp_client):
|
||||||
async def check(request):
|
async def check(request):
|
||||||
session = await get_session(request)
|
session = await get_session(request)
|
||||||
assert session['AIOHTTP_SECURITY'] == 'Andrew'
|
assert session['AIOHTTP_SECURITY'] == 'Andrew'
|
||||||
return web.Response()
|
return web.HTTPOk()
|
||||||
|
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.router.add_route('GET', '/', handler)
|
app.router.add_route('GET', '/', handler)
|
||||||
app.router.add_route('GET', '/check', check)
|
app.router.add_route('GET', '/check', check)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ async def test_remember(make_app, aiohttp_client):
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_identify(make_app, aiohttp_client):
|
async def test_identify(make_app, test_client):
|
||||||
|
|
||||||
async def create(request):
|
async def create(request):
|
||||||
response = web.Response()
|
response = web.Response()
|
||||||
|
@ -66,7 +66,7 @@ async def test_identify(make_app, aiohttp_client):
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.router.add_route('GET', '/', check)
|
app.router.add_route('GET', '/', check)
|
||||||
app.router.add_route('POST', '/', create)
|
app.router.add_route('POST', '/', create)
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
resp = await client.post('/')
|
resp = await client.post('/')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
@ -74,28 +74,28 @@ async def test_identify(make_app, aiohttp_client):
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
||||||
|
|
||||||
async def test_forget(make_app, aiohttp_client):
|
async def test_forget(make_app, test_client):
|
||||||
|
|
||||||
async def index(request):
|
async def index(request):
|
||||||
session = await get_session(request)
|
session = await get_session(request)
|
||||||
return web.Response(text=session.get('AIOHTTP_SECURITY', ''))
|
return web.HTTPOk(text=session.get('AIOHTTP_SECURITY', ''))
|
||||||
|
|
||||||
async def login(request):
|
async def login(request):
|
||||||
response = web.HTTPFound(location='/')
|
response = web.HTTPFound(location='/')
|
||||||
await remember(request, response, 'Andrew')
|
await remember(request, response, 'Andrew')
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
async def logout(request):
|
async def logout(request):
|
||||||
response = web.HTTPFound('/')
|
response = web.HTTPFound('/')
|
||||||
await forget(request, response)
|
await forget(request, response)
|
||||||
raise response
|
return response
|
||||||
|
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.router.add_route('GET', '/', index)
|
app.router.add_route('GET', '/', index)
|
||||||
app.router.add_route('POST', '/login', login)
|
app.router.add_route('POST', '/login', login)
|
||||||
app.router.add_route('POST', '/logout', logout)
|
app.router.add_route('POST', '/logout', logout)
|
||||||
|
|
||||||
client = await aiohttp_client(app)
|
client = await test_client(app)
|
||||||
|
|
||||||
resp = await client.post('/login')
|
resp = await client.post('/login')
|
||||||
assert 200 == resp.status
|
assert 200 == resp.status
|
||||||
|
|
Loading…
Reference in New Issue