Skip to content
This repository has been archived by the owner on Apr 9, 2023. It is now read-only.

Commit

Permalink
Merge pull request #22 from plone/auth-framework
Browse files Browse the repository at this point in the history
Rethink how authentication is done
  • Loading branch information
bloodbare committed Nov 23, 2016
2 parents 3f9756b + 6bab15f commit f4bc133
Show file tree
Hide file tree
Showing 24 changed files with 346 additions and 216 deletions.
2 changes: 1 addition & 1 deletion config-zeo.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"creator": {
"admin": "admin",
"password": "YWRtaW4="
"password": "admin"
},
"cors": {
"allow_origin": ["*"],
Expand Down
2 changes: 1 addition & 1 deletion config-zodb.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
],
"creator": {
"admin": "admin",
"password": "YWRtaW4="
"password": "admin"
},
"cors": {
"allow_origin": ["*"],
Expand Down
5 changes: 2 additions & 3 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
"static": [
{"favicon.ico": "static/favicon.ico"}
],
"creator": {
"admin": "admin",
"password": "YWRtaW4="
"root_user": {
"password": "admin"
},
"cors": {
"allow_origin": ["*"],
Expand Down
4 changes: 4 additions & 0 deletions src/plone.server/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
1.0a7 (unreleased)
------------------

- Remove `AUTH_USER_PLUGINS` and `AUTH_EXTRACTION_PLUGINS`. Authentication now
consists of auth policies, user identifiers and token checkers.
[vangheem]

- Correctly check parent object for allowed addable types
[vangheem]

Expand Down
8 changes: 4 additions & 4 deletions src/plone.server/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Creating default content

Once started, you will require to add at least a Plone site to start fiddling around::

curl -X POST -H "Accept: application/json" -H "Authorization: Basic YWRtaW4=" -H "Content-Type: application/json" -d '{
curl -X POST -H "Accept: application/json" -H "Authorization: Basic admin" -H "Content-Type: application/json" -d '{
"@type": "Site",
"title": "Plone 1",
"id": "plone",
Expand All @@ -47,15 +47,15 @@ Once started, you will require to add at least a Plone site to start fiddling ar

and give permissions to add content to it::

curl -X POST -H "Accept: application/json" -H "Authorization: Basic YWRtaW4=" -H "Content-Type: application/json" -d '{
curl -X POST -H "Accept: application/json" -H "Authorization: Basic admin" -H "Content-Type: application/json" -d '{
"prinrole": {
"Anonymous User": ["plone.Member", "plone.Reader"]
}
}' "http://127.0.0.1:8080/zodb1/plone/@sharing"

and create actual content::

curl -X POST -H "Accept: application/json" -H "Authorization: Basic YWRtaW4=" -H "Content-Type: application/json" -d '{
curl -X POST -H "Accept: application/json" -H "Authorization: Basic admin" -H "Content-Type: application/json" -d '{
"@type": "Item",
"title": "News",
"id": "news"
Expand All @@ -76,7 +76,7 @@ and for test coverage::
Default
-------

Default root access can be done with AUTHORIZATION header : Basic YWRtaW4=
Default root access can be done with AUTHORIZATION header : Basic admin


Running dependency graph
Expand Down
38 changes: 28 additions & 10 deletions src/plone.server/plone/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,34 @@

_ = MessageFactory('plone')

DICT_METHODS = {}
DICT_RENDERS = collections.OrderedDict()
DICT_LANGUAGES = {}
DEFAULT_LAYER = []
DEFAULT_PERMISSION = []
AVAILABLE_ADDONS = {}
JSON_API_DEFINITION = {}
AUTH_EXTRACTION_PLUGINS = []
AUTH_USER_PLUGINS = []
CORS = {}
app_settings = {
'databases': [],
'address': 8080,
'static': [],
'utilities': [],
'root_user': {
'id': 'admin',
'password': ''
},
'auth_policies': [
'plone.server.auth.policies.BearerAuthPolicy',
'plone.server.auth.policies.WSTokenAuthPolicy',
],
'auth_user_identifiers': [
'plone.server.auth.users.RootUserIdentifier'
],
'auth_token_checker': [
'plone.server.auth.checkers.SaltedHashPasswordChecker',
],
'default_layers': [],
'http_methods': {},
'renderers': collections.OrderedDict(),
'languages': {},
'default_permission': '',
'available_addons': {},
'api_definition': {},
'cors': {}
}

SCHEMA_CACHE = {}
PERMISSIONS_CACHE = {}
Expand Down
12 changes: 6 additions & 6 deletions src/plone.server/plone/server/api/addons.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from plone.server import _
from plone.server import AVAILABLE_ADDONS
from plone.server import app_settings
from plone.server.api.service import Service
from plone.server.browser import ErrorResponse
from plone.server.registry import IAddons
Expand All @@ -10,7 +10,7 @@ class Install(Service):
async def __call__(self):
data = await self.request.json()
id_to_install = data.get('id', None)
if id_to_install not in AVAILABLE_ADDONS:
if id_to_install not in app_settings['available_addons']:
return ErrorResponse(
'RequiredParam',
_("Property 'id' is required to be valid"))
Expand All @@ -22,7 +22,7 @@ async def __call__(self):
return ErrorResponse(
'Duplicate',
_("Addon already installed"))
handler = AVAILABLE_ADDONS[id_to_install]['handler']
handler = app_settings['available_addons'][id_to_install]['handler']
handler.install(self.request)
config.enabled |= {id_to_install}

Expand All @@ -31,7 +31,7 @@ class Uninstall(Service):
async def __call__(self):
data = await self.request.json()
id_to_install = data.get('id', None)
if id_to_install not in AVAILABLE_ADDONS:
if id_to_install not in app_settings['available_addons']:
return ErrorResponse(
'RequiredParam',
_("Property 'id' is required to be valid"))
Expand All @@ -44,7 +44,7 @@ async def __call__(self):
'Duplicate',
_("Addon not installed"))

handler = AVAILABLE_ADDONS[id_to_install]['handler']
handler = app_settings['available_addons'][id_to_install]['handler']
handler.uninstall(self.request)
config.enabled -= {id_to_install}

Expand All @@ -55,7 +55,7 @@ async def __call__(self):
'available': [],
'installed': []
}
for key, addon in AVAILABLE_ADDONS.items():
for key, addon in app_settings['available_addons'].items():
result['available'].append({
'id': key,
'title': addon['title']
Expand Down
4 changes: 2 additions & 2 deletions src/plone.server/plone/server/api/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from plone.server import JSON_API_DEFINITION
from plone.server import app_settings
from plone.server.api.service import Service
from plone.server.json.interfaces import IResourceSerializeToJson
from zope.component import getMultiAdapter
Expand All @@ -20,4 +20,4 @@ async def __call__(self):

class GetAPIDefinition(Service):
async def __call__(self):
return JSON_API_DEFINITION
return app_settings['api_definition']
12 changes: 6 additions & 6 deletions src/plone.server/plone/server/api/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from plone.server.json.exceptions import DeserializationError
from plone.server.json.interfaces import IResourceDeserializeFromJson
from plone.server.json.interfaces import IResourceSerializeToJson
from plone.server import CORS
from plone.server import app_settings
from plone.server.utils import get_authenticated_user_id
from plone.server.utils import iter_parents
from random import randint
Expand Down Expand Up @@ -201,7 +201,7 @@ async def preflight(self):
"""We need to check if there is cors enabled and is valid."""
headers = {}

if not CORS:
if not app_settings['cors']:
return {}

origin = self.request.headers.get('Origin', None)
Expand All @@ -220,13 +220,13 @@ async def preflight(self):
requested_headers = map(str.strip, requested_headers.split(', '))

requested_method = requested_method.upper()
allowed_methods = CORS['allow_methods']
allowed_methods = app_settings['cors']['allow_methods']
if requested_method not in allowed_methods:
raise HTTPMethodNotAllowed(
requested_method, allowed_methods,
text='Access-Control-Request-Method Method not allowed')

supported_headers = CORS['allow_headers']
supported_headers = app_settings['cors']['allow_headers']
if '*' not in supported_headers and requested_headers:
supported_headers = [s.lower() for s in supported_headers]
for h in requested_headers:
Expand All @@ -242,8 +242,8 @@ async def preflight(self):
headers['Access-Control-Allow-Headers'] = ','.join(
supported_headers)
headers['Access-Control-Allow-Methods'] = ','.join(
CORS['allow_methods'])
headers['Access-Control-Max-Age'] = str(CORS['max_age'])
app_settings['cors']['allow_methods'])
headers['Access-Control-Max-Age'] = str(app_settings['cors']['max_age'])
return headers

async def render(self):
Expand Down
21 changes: 19 additions & 2 deletions src/plone.server/plone/server/api/ws.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
from aiohttp import web
from plone.server import DICT_METHODS
from plone.server import app_settings
from plone.server import jose
from datetime import datetime
from datetime import timedelta
from plone.server.api.service import Service
from plone.server.browser import Response
from plone.server.interfaces import ITraversableView
Expand All @@ -17,6 +20,20 @@
logger = logging.getLogger(__name__)


def generate_websocket_token(self, real_token):
exp = datetime.utcnow() + timedelta(
seconds=self._websockets_ttl)

claims = {
'iat': int(datetime.utcnow().timestamp()),
'exp': int(exp.timestamp()),
'token': real_token
}
jwe = jose.encrypt(claims, app_settings['rsa']['priv'])
token = jose.serialize_compact(jwe)
return token.decode('utf-8')


class WebsocketGetToken(Service):

async def __call__(self):
Expand Down Expand Up @@ -49,7 +66,7 @@ async def __call__(self):
if message['op'] == 'close':
await ws.close()
elif message['op'] == 'GET':
method = DICT_METHODS['GET']
method = app_settings['http_methods']['GET']
path = tuple(p for p in message['value'].split('/') if p)
obj, tail = await do_traverse(
self.request, self.request.site, path)
Expand Down
29 changes: 29 additions & 0 deletions src/plone.server/plone/server/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from plone.server import app_settings
from plone.server.utils import resolve_or_get


async def authenticate_request(request):
for policy in app_settings['auth_policies']:
policy = resolve_or_get(policy)
token = await policy(request).extract_token()
if token:
user = await find_user(request, token)
if user:
if await authenticate_user(request, user, token):
return user


async def find_user(request, token):
for identifier in app_settings['auth_user_identifiers']:
identifier = resolve_or_get(identifier)
user = await identifier(request).get_user()
if user:
return user


async def authenticate_user(request, user, token):
for checker in app_settings['auth_token_checker']:
checker = resolve_or_get(checker)
if await checker(request).validate(user, token):
return True
return False
33 changes: 33 additions & 0 deletions src/plone.server/plone/server/auth/checkers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from plone.server.utils import strings_differ

import hashlib
import uuid


def hash_password(password, salt=None):
if salt is None:
salt = uuid.uuid4().hex

if isinstance(salt, str):
salt = salt.encode('utf-8')

if isinstance(password, str):
password = password.encode('utf-8')

hashed_password = hashlib.sha512(password + salt).hexdigest()
return '{}:{}'.format(salt.decode('utf-8'), hashed_password)


class SaltedHashPasswordChecker(object):

def __init__(self, request):
self.request = request

async def validate(self, user, token):
user_pw = getattr(user, 'password', None)
if (not user_pw or
':' not in user_pw or
'password' not in token):
return False
salt = user.password.split(':')[0]
return not strings_differ(hash_password(token['password'], salt), user_pw)
31 changes: 16 additions & 15 deletions src/plone.server/plone/server/auth/participation.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
from plone.server.auth import authenticate_request
from plone.server.interfaces import IRequest
from plone.server import AUTH_EXTRACTION_PLUGINS
from plone.server import AUTH_USER_PLUGINS
from plone.server.transactions import get_current_request
from zope.component import adapter
from zope.interface import implementer
from zope.security.interfaces import IParticipation


class RootParticipation(object):
ROOT_USER_ID = 'RootUser'

def __init__(self, request):
self.principal = PloneUser(request)
self.principal.id = 'RootUser'

self.principal._groups.append('Managers')
self.interaction = None
class RootUser(object):
def __init__(self, password):
self.id = ROOT_USER_ID
self.password = password
self.groups = ['Managers']
self._roles = {}
self._properties = {}


class AnonymousParticipation(object):
Expand Down Expand Up @@ -62,21 +63,21 @@ def __init__(self, request, ident):
@adapter(IRequest)
@implementer(IParticipation)
class PloneParticipation(object):
principal = None

def __init__(self, request):
self.request = request

async def __call__(self):
# Cached user
if not hasattr(self.request, '_cache_user'):
user = await authenticate_request(self.request)
if user:
self.request._cache_user = user
self.principal = user
else:
self.principal = getattr(self.request, '_cache_user', None)

for plugin in AUTH_EXTRACTION_PLUGINS:
await plugin(self.request).extract_user()

for plugin in AUTH_USER_PLUGINS:
await plugin(self.request).create_user()

self.principal = getattr(self.request, '_cache_user', None)
self.interaction = None


Expand Down

0 comments on commit f4bc133

Please sign in to comment.