View
@@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
import json
from functools import partial
from uuid import uuid1, uuid4, UUID
from annotator import auth
from hem.interfaces import IDBSession
from hem.db import get_session
from horus import interfaces
from horus.models import (
BaseModel,
ActivationMixin,
GroupMixin,
UserMixin,
UserGroupMixin,
)
from horus.strings import UIStringsBase
from pyramid_basemodel import Base, Session
from pyramid.settings import asbool
from pyramid.threadlocal import get_current_request
import sqlalchemy as sa
from sqlalchemy import func, or_
from sqlalchemy.dialects import postgresql as pg
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.schema import Column
from sqlalchemy.types import Integer, TypeDecorator, CHAR, VARCHAR
import transaction
from zope.interface import Interface
class JSONEncodedDict(TypeDecorator):
"""Represents an immutable structure as a json-encoded string.
Usage::
JSONEncodedDict(255)
"""
# pylint: disable=too-many-public-methods
impl = VARCHAR
def process_bind_param(self, value, dialect):
if value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
def python_type(self):
return dict
class GUID(TypeDecorator):
"""Platform-independent GUID type.
From http://docs.sqlalchemy.org/en/latest/core/types.html
Copyright (C) 2005-2011 the SQLAlchemy authors and contributors
Uses Postgresql's UUID type, otherwise uses
CHAR(32), storing as stringified hex values.
"""
# pylint: disable=too-many-public-methods
impl = CHAR
def load_dialect_impl(self, dialect):
if dialect.name == 'postgresql':
return dialect.type_descriptor(pg.UUID())
else:
return dialect.type_descriptor(CHAR(32))
def process_bind_param(self, value, dialect):
if value is None:
return value
elif dialect.name == 'postgresql':
return str(value)
else:
if not isinstance(value, UUID):
return "%.32x" % UUID(value)
else:
# hexstring
return "%.32x" % value
def process_result_value(self, value, dialect):
if value is None:
return value
else:
return UUID(value)
def python_type(self):
return UUID
class Activation(ActivationMixin, Base):
def __init__(self, *args, **kwargs):
super(Activation, self).__init__(*args, **kwargs)
# XXX: Horus currently has a bug where the Activation model isn't
# flushed before the email is generated, causing the link to be
# broken (hypothesis/h#1156).
#
# Fixed in horus@90f838cef12be249a9e9deb5f38b37151649e801
request = get_current_request()
db = get_session(request)
db.add(self)
db.flush()
class ConsumerMixin(BaseModel):
"""
API Consumer
The annotator-store :py:class:`annotator.auth.Authenticator` uses this
function in the process of authenticating requests to verify the secrets of
the JSON Web Token passed by the consumer client.
"""
key = Column(GUID, default=partial(uuid1, clock_seq=id(Base)), index=True)
secret = Column(GUID, default=uuid4)
ttl = Column(Integer, default=auth.DEFAULT_TTL)
def __init__(self, **kwargs):
super(ConsumerMixin, self).__init__()
self.__dict__.update(kwargs)
def __repr__(self):
return '<Consumer %r>' % self.key
@classmethod
def get_by_key(cls, request, key):
return get_session(request).query(cls).filter(cls.key == key).first()
class IConsumerClass(Interface):
pass
class Consumer(ConsumerMixin, Base):
pass
class Group(GroupMixin, Base):
pass
class User(UserMixin, Base):
# pylint: disable=too-many-public-methods
@classmethod
def get_by_username(cls, request, username):
session = get_session(request)
lhs = func.replace(cls.username, '.', '')
rhs = username.replace('.', '')
return session.query(cls).filter(
func.lower(lhs) == rhs.lower()
).first()
@classmethod
def get_by_username_or_email(cls, request, username, email):
session = get_session(request)
lhs = func.replace(cls.username, '.', '')
rhs = username.replace('.', '')
return session.query(cls).filter(
or_(
func.lower(lhs) == rhs.lower(),
cls.email == email
)
).first()
@property
def email_confirmed(self):
return bool((self.status or 0) & 0b001)
@email_confirmed.setter
def email_confirmed(self, value):
if value:
self.status = (self.status or 0) | 0b001
else:
self.status = (self.status or 0) & ~0b001
@property
def optout(self):
return bool((self.status or 0) & 0b010)
@optout.setter
def optout(self, value):
if value:
self.status = (self.status or 0) | 0b010
else:
self.status = (self.status or 0) & ~0b010
@property
def subscriptions(self):
return bool((self.status or 0) & 0b100)
@subscriptions.setter
def subscriptions(self, value):
if value:
self.status = (self.status or 0) | 0b100
else:
self.status = (self.status or 0) & ~0b100
class UserGroup(UserGroupMixin, Base):
pass
class UserSubscriptionsMixin(BaseModel):
# pylint: disable=no-self-use
@declared_attr
def username(self):
return sa.Column(
sa.Unicode(30),
sa.ForeignKey(
'%s.%s' % (UserMixin.__tablename__, 'username'),
onupdate='CASCADE',
ondelete='CASCADE'
),
nullable=False
)
@declared_attr
def query(self):
return sa.Column(JSONEncodedDict(4096), nullable=False)
@declared_attr
def template(self):
return sa.Column(
sa.Enum('reply_notification', 'custom_search',
name='subscription_template'),
nullable=False,
default='custom_search'
)
@declared_attr
def description(self):
return sa.Column(sa.VARCHAR(256), default="")
@declared_attr
def type(self):
return sa.Column(
sa.Enum('system', 'user', name='subscription_type'),
nullable=False,
default='user'
)
@declared_attr
def active(self):
return sa.Column(sa.BOOLEAN, default=True, nullable=False)
class UserSubscriptions(UserSubscriptionsMixin, Base):
pass
def includeme(config):
registry = config.registry
settings = registry.settings
config.include('pyramid_basemodel')
config.include('pyramid_tm')
models = [
(interfaces.IActivationClass, Activation),
(interfaces.IUserClass, User),
(interfaces.IUIStrings, UIStringsBase),
(IConsumerClass, Consumer),
(IDBSession, Session)
]
for iface, imp in models:
if not registry.queryUtility(iface):
registry.registerUtility(imp, iface)
if asbool(settings.get('basemodel.should_create_all', True)):
key = settings['api.key']
secret = settings.get('api.secret')
ttl = settings.get('api.ttl', auth.DEFAULT_TTL)
session = Session()
consumer = session.query(Consumer).filter(Consumer.key == key).first()
if not consumer:
with transaction.manager:
consumer = Consumer(key=key, secret=secret, ttl=ttl)
session.add(consumer)
session.flush()
View
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
import re
from urllib import quote
from annotator import auth
from oauthlib.oauth2 import BackendApplicationServer, RequestValidator
from pyramid.authentication import RemoteUserAuthenticationPolicy
from h.auth.local.models import Consumer, User
def generate_token(request):
message = {
'consumerKey': request.client.key,
'ttl': request.client.ttl,
}
if request.extra_credentials is not None:
message.update(request.extra_credentials)
return auth.encode_token(message, request.client.secret)
def get_consumer(request, key):
inst = Consumer.get_by_key(request, key)
# Coerce types so elasticsearch doesn't choke on the UUIDs.
# TODO: Can we add magic to .models.GUID to take care of this?
result = auth.Consumer(str(key))
result.secret = str(inst.secret)
result.ttl = inst.ttl
return result
class RequestValidator(RequestValidator):
def __init__(self, request, *args, **kwargs):
super(RequestValidator, self).__init__(*args, **kwargs)
# bw compat
if request.params.get('grant_type') is None:
persona = request.GET.get('persona')
if persona is not None:
request.GET['persona'] = quote(persona)
request.GET['grant_type'] = 'client_credentials'
self.request = request
def authenticate_client(self, request):
key = self.request.registry.settings['api.key']
request.client = get_consumer(self.request, key)
request.client.client_id = key
request.client_id = key
return True
def get_default_redirect_uri(self, client_id, request):
return self.request.registry.settings.get('horus.login_redirect', '/')
def get_default_scopes(self, client, request):
return ['annotations']
def save_bearer_token(self, token, request):
return self.get_default_redirect_uri(request.client_id, request)
def validate_bearer_token(self, token, scopes, request):
self.authenticate_client(request)
try:
credentials = auth.decode_token(token, request.client.secret)
request.user = credentials.get('userId')
return True
except:
pass
return False
def validate_grant_type(self, client_id, grant_type, client, request):
if grant_type == 'client_credentials':
return client_id == client.client_id
return False
def validate_scopes(self, client_id, scopes, client, request):
return scopes == ['annotations']
class LocalAuthenticationPolicy(RemoteUserAuthenticationPolicy):
def unauthenticated_userid(self, request):
validator = RequestValidator(request)
token_generator = generate_token
server = BackendApplicationServer(validator, token_generator)
token = request.environ.get(self.environ_key)
if token is not None:
request.authorization = 'Bearer %s' % token
valid, r = server.verify_request(
request.url,
request.method,
None,
request.headers,
['annotations'],
)
if valid:
return r.user
return None
def groupfinder(userid, request):
username = re.match('acct:([^@]+)@', userid).group(1)
user = User.get_by_username(request, username)
groups = []
if user:
groups = [userid]
for group in user.groups:
groups.append('group:%s' % group.name)
return groups
def includeme(config):
registry = config.registry
settings = registry.settings
authn_debug = settings.get('pyramid.debug_authorization') \
or settings.get('debug_authorization')
authn_policy = LocalAuthenticationPolicy(
environ_key='HTTP_X_ANNOTATOR_AUTH_TOKEN',
callback=groupfinder,
debug=authn_debug,
)
config.set_authentication_policy(authn_policy)
View
@@ -2,11 +2,12 @@
# pylint: disable=no-init, too-few-public-methods
import colander
import deform
from hem.db import get_session
from horus import interfaces
from horus.schemas import unique_email
from pyramid.session import check_csrf_token
from h import interfaces
from h.models import _, get_session
from h.models import _
@colander.deferred
@@ -93,7 +94,7 @@ class ResetPasswordSchema(CSRFSchema):
class ActivateSchema(CSRFSchema):
code = colander.SchemaNode(
colander.String(),
title="Security Code"
title=_("Security Code")
)
password = colander.SchemaNode(
colander.String(),
@@ -111,7 +112,6 @@ def includeme(config):
(interfaces.IRegisterSchema, RegisterSchema),
(interfaces.IForgotPasswordSchema, ForgotPasswordSchema),
(interfaces.IResetPasswordSchema, ResetPasswordSchema),
(interfaces.IActivateSchema, ActivateSchema),
]
for iface, imp in schemas:
View
@@ -0,0 +1,42 @@
from pyramid.events import subscriber
from pyramid.settings import asbool
from horus.events import (
NewRegistrationEvent,
RegistrationActivatedEvent,
PasswordResetEvent,
)
from h.events import LoginEvent
@subscriber(NewRegistrationEvent, autologin=True)
@subscriber(PasswordResetEvent, autologin=True)
@subscriber(RegistrationActivatedEvent)
def login(event):
request = event.request
user = 'acct:%s@%s' % (event.user.username, request.server_name)
event = LoginEvent(request, user)
request.registry.notify(event)
class AutoLogin(object):
# pylint: disable=too-few-public-methods
def __init__(self, val, config):
self.val = val
def text(self):
return 'autologin = %s' % (self.val,)
phash = text
def __call__(self, event):
request = event.request
settings = request.registry.settings
autologin = asbool(settings.get('horus.autologin', False))
return self.val == autologin
def includeme(config):
config.add_subscriber_predicate('autologin', AutoLogin)
config.scan(__name__)
View
@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
import json
from urllib import unquote
import colander
import deform
import horus.views
from horus.lib import FlashMessage
from pyramid import httpexceptions
from pyramid.view import view_config, view_defaults
from h import events, views
from h.auth.local import forms, models, oauth, schemas
from h.models import _
def ajax_form(request, result):
errors = []
if isinstance(result, httpexceptions.HTTPRedirection):
request.response.headers.extend(result.headers)
result = {'status': 'okay'}
elif isinstance(result, httpexceptions.HTTPError):
request.response.status_code = result.code
result = {'status': 'failure', 'reason': str(result)}
else:
errors = result.pop('errors', [])
if errors:
request.response.status_code = 400
result['status'] = 'failure'
result['reason'] = _('Please check your input.')
else:
result['status'] = 'okay'
for e in errors:
if isinstance(e, colander.Invalid):
result.setdefault('errors', {})
result['errors'].update(e.asdict())
return result
class AsyncFormViewMapper(object):
def __init__(self, **kw):
self.attr = kw['attr']
def __call__(self, view):
def wrapper(context, request):
if request.method == 'POST':
data = request.json_body
data.update(request.params)
request.content_type = 'application/x-www-form-urlencoded'
request.POST.clear()
request.POST.update(data)
inst = view(request)
meth = getattr(inst, self.attr)
result = meth()
result = ajax_form(request, result)
result['flash'] = views.pop_flash(request)
result['model'] = views.model(request)
result.pop('form', None)
return result
return wrapper
@view_defaults(accept='text/html', renderer='h:templates/auth.pt')
@view_config(attr='login', route_name='login')
@view_config(attr='logout', route_name='logout')
class AuthController(horus.views.AuthController):
def login(self):
request = self.request
result = super(AuthController, self).login()
if request.user:
# XXX: Horus should maybe do this for us
user = 'acct:%s@%s' % (request.user.username, request.server_name)
event = events.LoginEvent(request, user)
request.registry.notify(event)
return result
def logout(self):
request = self.request
result = super(AuthController, self).logout()
# XXX: Horus should maybe do this for us
event = events.LogoutEvent(request)
request.registry.notify(event)
return result
@view_defaults(accept='application/json', name='app', renderer='json')
@view_config(attr='login', request_param='__formid__=login')
@view_config(attr='logout', request_param='__formid__=logout')
class AsyncAuthController(AuthController):
__view_mapper__ = AsyncFormViewMapper
@view_defaults(accept='text/html', renderer='h:templates/auth.pt')
@view_config(attr='forgot_password', route_name='forgot_password')
@view_config(attr='reset_password', route_name='reset_password')
class ForgotPasswordController(horus.views.ForgotPasswordController):
pass
@view_defaults(accept='application/json', name='app', renderer='json')
@view_config(attr='forgot_password', request_param='__formid__=forgot')
class AsyncForgotPasswordController(ForgotPasswordController):
__view_mapper__ = AsyncFormViewMapper
@view_defaults(accept='text/html', renderer='h:templates/auth.pt')
@view_config(attr='register', route_name='register')
@view_config(attr='activate', route_name='activate')
class RegisterController(horus.views.RegisterController):
pass
@view_defaults(accept='application/json', name='app', renderer='json')
@view_config(attr='register', request_param='__formid__=register')
@view_config(attr='activate', request_param='__formid__=activate')
class AsyncRegisterController(RegisterController):
__view_mapper__ = AsyncFormViewMapper
def activate(self):
"""Activate a user and set a password given an activation code.
This view is different from the activation view in horus because it
does not require the user id to be passed. It trusts the activation
code and updates the password.
"""
request = self.request
Str = self.Str
schema = schemas.ActivationSchema.bind(request=request)
form = forms.ActivateForm(schema)
appstruct = None
try:
appstruct = form.validate(request.POST.items())
except deform.ValidationFailure as e:
return dict(errors=e.error.children)
code = appstruct['code']
activation = models.Activation.get_by_code(request, code)
user = None
if activation:
user = self.User.get_by_activation(request, activation)
if user is None:
return dict(errors=[_('This activation code is not valid.')])
user.password = appstruct['password']
self.db.delete(activation)
self.db.add(user)
FlashMessage(request, Str.reset_password_done, kind='success')
# XXX: Horus should maybe do this for us
event = events.RegistrationActivatedEvent(request, user, activation)
request.registry.notify(event)
return {}
def access_token(request):
validator = oauth.RequestValidator(request)
token_generator = oauth.generate_token
server = oauth.BackendApplicationServer(validator, token_generator)
persona = unquote(request.params.get('persona'))
personas = request.session.get('personas', [])
try:
credentials = dict(userId=next(p for p in personas if p == persona))
except StopIteration:
credentials = None
headers, body, status = server.create_token_response(
request.url,
request.method,
request.body,
request.headers,
credentials,
)
request.response.headers.update(headers)
request.response.status_int = status
request.response.content_type = 'application/json'
request.response.charset = 'UTF-8'
request.response.body = body
return request.response
@view_config(renderer='string', route_name='token')
def token(request):
return json.loads(access_token(request).body).get('access_token', '')
def includeme(config):
registry = config.registry
settings = registry.settings
token_url = settings.get('auth.token_endpoint', '/api/token').strip('/')
config.add_route('token', token_url)
config.include('horus')
config.scan(__name__)
View
@@ -1,19 +1,10 @@
# -*- coding: utf-8 -*-
from horus.events import (
NewRegistrationEvent,
RegistrationActivatedEvent,
PasswordResetEvent,
ProfileUpdatedEvent,
)
from pyramid.events import BeforeRender, NewRequest, NewResponse
__all__ = [
'AnnotationEvent',
'LoginEvent',
'LogoutEvent',
'NewRegistrationEvent',
'RegistrationActivatedEvent',
'PasswordResetEvent',
'ProfileUpdatedEvent',
'BeforeRender',
'NewRequest',
'NewResponse',
View
@@ -1,26 +1,5 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-public-methods
from hem.interfaces import IDBSession
from horus.interfaces import (
IUserClass,
IActivationClass,
ILoginForm,
IRegisterForm,
IForgotPasswordForm,
IResetPasswordForm,
IProfileForm,
ILoginSchema,
IRegisterSchema,
IForgotPasswordSchema,
IResetPasswordSchema,
IProfileSchema,
IUIStrings,
)
from zope.interface import Interface
__all__ = [
@@ -48,22 +27,8 @@
class IAnnotationClass(Interface):
pass
class IConsumerClass(Interface):
pass
class IStoreClass(Interface):
pass
class IActivateForm(Interface):
pass
class IActivateSchema(Interface):
pass
class IStreamResource(Interface):
pass
View
@@ -1,106 +1,14 @@
# -*- coding: utf-8 -*-
import json
from functools import partial
from uuid import uuid1, uuid4, UUID
from annotator import annotation, document
from annotator.auth import DEFAULT_TTL
from horus.models import (
get_session,
BaseModel,
ActivationMixin,
GroupMixin,
UserMixin,
UserGroupMixin,
)
from horus.strings import UIStringsBase
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.decorator import reify
from pyramid.i18n import TranslationStringFactory
from pyramid.threadlocal import get_current_request
from pyramid.security import Allow, Authenticated, Everyone, ALL_PERMISSIONS
from pyramid.settings import asbool
from pyramid_basemodel import Base, Session
import sqlalchemy as sa
from sqlalchemy import func, or_
from sqlalchemy.dialects import postgresql as pg
from sqlalchemy.schema import Column
from sqlalchemy.types import Integer, TypeDecorator, CHAR, VARCHAR
from sqlalchemy.ext.declarative import declared_attr
import transaction
from h import interfaces
_ = TranslationStringFactory(__package__)
class JSONEncodedDict(TypeDecorator):
"""Represents an immutable structure as a json-encoded string.
Usage::
JSONEncodedDict(255)
"""
# pylint: disable=too-many-public-methods
impl = VARCHAR
def process_bind_param(self, value, dialect):
if value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
def python_type(self):
return dict
class GUID(TypeDecorator):
"""Platform-independent GUID type.
From http://docs.sqlalchemy.org/en/latest/core/types.html
Copyright (C) 2005-2011 the SQLAlchemy authors and contributors
Uses Postgresql's UUID type, otherwise uses
CHAR(32), storing as stringified hex values.
"""
# pylint: disable=too-many-public-methods
impl = CHAR
def load_dialect_impl(self, dialect):
if dialect.name == 'postgresql':
return dialect.type_descriptor(pg.UUID())
else:
return dialect.type_descriptor(CHAR(32))
def process_bind_param(self, value, dialect):
if value is None:
return value
elif dialect.name == 'postgresql':
return str(value)
else:
if not isinstance(value, UUID):
return "%.32x" % UUID(value)
else:
# hexstring
return "%.32x" % value
def process_result_value(self, value, dialect):
if value is None:
return value
else:
return UUID(value)
def python_type(self):
return UUID
class Annotation(annotation.Annotation):
def __acl__(self):
acl = []
@@ -293,218 +201,13 @@ class Document(document.Document):
pass
class ConsumerMixin(BaseModel):
"""
API Consumer
The annotator-store :py:class:`annotator.auth.Authenticator` uses this
function in the process of authenticating requests to verify the secrets of
the JSON Web Token passed by the consumer client.
"""
key = Column(GUID, default=partial(uuid1, clock_seq=id(Base)), index=True)
secret = Column(GUID, default=uuid4)
ttl = Column(Integer, default=DEFAULT_TTL)
def __init__(self, **kwargs):
super(ConsumerMixin, self).__init__()
self.__dict__.update(kwargs)
def __repr__(self):
return '<Consumer %r>' % self.key
@classmethod
def get_by_key(cls, request, key):
return get_session(request).query(cls).filter(cls.key == key).first()
class Activation(ActivationMixin, Base):
def __init__(self, *args, **kwargs):
super(Activation, self).__init__(*args, **kwargs)
# XXX: Horus currently has a bug where the Activation model isn't
# flushed before the email is generated, causing the link to be
# broken (hypothesis/h#1156).
#
# Fixed in horus@90f838cef12be249a9e9deb5f38b37151649e801
request = get_current_request()
db = get_session(request)
db.add(self)
db.flush()
class Consumer(ConsumerMixin, Base):
pass
class Group(GroupMixin, Base):
pass
class User(UserMixin, Base):
# pylint: disable=too-many-public-methods
@classmethod
def get_by_username(cls, request, username):
session = get_session(request)
lhs = func.replace(cls.username, '.', '')
rhs = username.replace('.', '')
return session.query(cls).filter(
func.lower(lhs) == rhs.lower()
).first()
@classmethod
def get_by_username_or_email(cls, request, username, email):
session = get_session(request)
lhs = func.replace(cls.username, '.', '')
rhs = username.replace('.', '')
return session.query(cls).filter(
or_(
func.lower(lhs) == rhs.lower(),
cls.email == email
)
).first()
@property
def email_confirmed(self):
return bool((self.status or 0) & 0b001)
@email_confirmed.setter
def email_confirmed(self, value):
if value:
self.status = (self.status or 0) | 0b001
else:
self.status = (self.status or 0) & ~0b001
@property
def optout(self):
return bool((self.status or 0) & 0b010)
@optout.setter
def optout(self, value):
if value:
self.status = (self.status or 0) | 0b010
else:
self.status = (self.status or 0) & ~0b010
@property
def subscriptions(self):
return bool((self.status or 0) & 0b100)
@subscriptions.setter
def subscriptions(self, value):
if value:
self.status = (self.status or 0) | 0b100
else:
self.status = (self.status or 0) & ~0b100
class UserGroup(UserGroupMixin, Base):
pass
class UserSubscriptionsMixin(BaseModel):
# pylint: disable=no-self-use
@declared_attr
def username(self):
return sa.Column(
sa.Unicode(30),
sa.ForeignKey(
'%s.%s' % (UserMixin.__tablename__, 'username'),
onupdate='CASCADE',
ondelete='CASCADE'
),
nullable=False
)
@declared_attr
def query(self):
return sa.Column(JSONEncodedDict(4096), nullable=False)
@declared_attr
def template(self):
return sa.Column(
sa.Enum('reply_notification', 'custom_search',
name='subscription_template'),
nullable=False,
default='custom_search'
)
@declared_attr
def description(self):
return sa.Column(sa.VARCHAR(256), default="")
@declared_attr
def type(self):
return sa.Column(
sa.Enum('system', 'user', name='subscription_type'),
nullable=False,
default='user'
)
@declared_attr
def active(self):
return sa.Column(sa.BOOLEAN, default=True, nullable=False)
class UserSubscriptions(UserSubscriptionsMixin, Base):
pass
def groupfinder(userid, request):
user = request.user
groups = None
if user:
groups = []
for group in user.groups:
groups.append('group:%s' % group.name)
groups.append('acct:%s@%s' % (user.username, request.server_name))
return groups
def includeme(config):
registry = config.registry
settings = registry.settings
authn_debug = settings.get('pyramid.debug_authorization') \
or settings.get('debug_authorizations')
authn_policy = AuthTktAuthenticationPolicy(
settings.get('auth.secret', uuid4().hex + uuid4().hex),
callback=groupfinder,
hashalg='sha512',
debug=authn_debug
)
config.set_authentication_policy(authn_policy)
config.include('pyramid_basemodel')
config.include('pyramid_tm')
models = [
(interfaces.IDBSession, Session),
(interfaces.IUserClass, User),
(interfaces.IConsumerClass, Consumer),
(interfaces.IActivationClass, Activation),
(interfaces.IAnnotationClass, Annotation),
(interfaces.IUIStrings, UIStringsBase),
]
for iface, imp in models:
if not registry.queryUtility(iface):
registry.registerUtility(imp, iface)
if asbool(settings.get('basemodel.should_create_all', True)):
key = settings['api.key']
secret = settings.get('api.secret')
ttl = settings.get('api.ttl', DEFAULT_TTL)
session = Session()
consumer = session.query(Consumer).filter(Consumer.key == key).first()
if not consumer:
with transaction.manager:
consumer = Consumer(key=key, secret=secret, ttl=ttl)
session.add(consumer)
session.flush()
View
@@ -10,7 +10,8 @@
from pyramid.renderers import render
from pyramid.events import subscriber
from h import events, models
from h import events
from h.auth.local import models
from h.streamer import FilterHandler, parent_values
log = logging.getLogger(__name__) # pylint: disable=invalid-name
@@ -286,11 +287,6 @@ def create_default_subscription(request, user):
session.flush()
@subscriber(events.NewRegistrationEvent)
def registration_subscriptions(event):
create_default_subscription(event.request, event.user)
@subscriber(events.LoginEvent)
def login_subscriptions(event):
if not event.user.subscriptions:
View
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from pyramid.events import subscriber
from pyramid.renderers import get_renderer
from pyramid.settings import asbool
from h import events
@@ -42,41 +41,22 @@ def set_csrf_cookie(event):
@subscriber(events.LoginEvent)
@subscriber(events.NewRegistrationEvent, autologin=True)
@subscriber(events.PasswordResetEvent, autologin=True)
@subscriber(events.RegistrationActivatedEvent)
def login(event):
request = event.request
user = event.user
session = request.session
persona = 'acct:%s@%s' % (user.username, request.server_name)
personas = session.setdefault('personas', [])
if persona not in personas:
personas.append(persona)
if user not in personas:
personas.append(user)
session.changed()
class AutoLogin(object):
# pylint: disable=too-few-public-methods
def __init__(self, val, config):
self.env = config.get_webassets_env()
self.val = val
def text(self):
return 'autologin = %s' % (self.val,)
phash = text
def __call__(self, event):
request = event.request
settings = request.registry.settings
autologin = asbool(settings.get('horus.autologin', False))
return self.val == autologin
@subscriber(events.LogoutEvent)
def logout(event):
event.request.session.invalidate()
def includeme(config):
config.add_subscriber_predicate('autologin', AutoLogin)
config.scan(__name__)
View
@@ -2,46 +2,16 @@
import json
import logging
import colander
import deform
from horus import views
from horus.lib import FlashMessage
from pyramid import httpexceptions
from pyramid.view import view_config, view_defaults
from h import events, interfaces
from h import interfaces
from h.models import _
from h.streamer import url_values_from_document
log = logging.getLogger(__name__) # pylint: disable=invalid-name
def ajax_form(request, result):
errors = []
if isinstance(result, httpexceptions.HTTPRedirection):
request.response.headers.extend(result.headers)
result = {'status': 'okay'}
elif isinstance(result, httpexceptions.HTTPError):
request.response.status_code = result.code
result = {'status': 'failure', 'reason': str(result)}
else:
errors = result.pop('errors', [])
if errors:
request.response.status_code = 400
result['status'] = 'failure'
result['reason'] = _('Please check your input.')
else:
result['status'] = 'okay'
for e in errors:
if isinstance(e, colander.Invalid):
result.setdefault('errors', {})
result['errors'].update(e.asdict())
return result
def pop_flash(request):
session = request.session
@@ -66,29 +36,6 @@ def model(request):
personas=session.get('personas', []))
class AsyncFormViewMapper(object):
def __init__(self, **kw):
self.attr = kw['attr']
def __call__(self, view):
def wrapper(context, request):
if request.method == 'POST':
data = request.json_body
data.update(request.params)
request.content_type = 'application/x-www-form-urlencoded'
request.POST.clear()
request.POST.update(data)
inst = view(request)
meth = getattr(inst, self.attr)
result = meth()
result = ajax_form(request, result)
result['flash'] = pop_flash(request)
result['model'] = model(request)
result.pop('form', None)
return result
return wrapper
@view_config(accept='application/json', name='app', renderer='json')
def app(request):
return dict(status='okay', flash=pop_flash(request), model=model(request))
@@ -98,10 +45,11 @@ def app(request):
context='pyramid.exceptions.BadCSRFToken')
def bad_csrf_token(context, request):
reason = _('Session is invalid. Please try again.')
exception = httpexceptions.HTTPBadRequest(reason)
result = ajax_form(request, exception)
result['model'] = model(request)
return result
return {
'status': 'failure',
'reason': reason,
'model': model(request),
}
@view_config(name='embed.js', renderer='templates/embed.txt')
@@ -116,11 +64,10 @@ def page(context, request):
@view_defaults(context='h.models.Annotation', layout='annotation')
class AnnotationController(views.BaseController):
class AnnotationController(object):
def __init__(self, request):
super(AnnotationController, self).__init__(request)
getUtility = request.registry.getUtility
self.Store = getUtility(interfaces.IStoreClass)
self.request = request
self.Store = request.registry.getUtility(interfaces.IStoreClass)
@view_config(accept='text/html', renderer='templates/displayer.pt')
def __html__(self):
@@ -155,108 +102,6 @@ def __call__(self):
return self.request.context
@view_defaults(accept='text/html', renderer='templates/auth.pt')
@view_config(attr='login', route_name='login')
@view_config(attr='logout', route_name='logout')
class AuthController(views.AuthController):
def login(self):
request = self.request
result = super(AuthController, self).login()
if request.user:
# XXX: Horus should maybe do this for us
event = events.LoginEvent(request, request.user)
request.registry.notify(event)
return result
def logout(self):
request = self.request
result = super(AuthController, self).logout()
# XXX: Horus should maybe do this for us
event = events.LogoutEvent(request)
request.registry.notify(event)
return result
@view_defaults(accept='application/json', name='app', renderer='json')
@view_config(attr='login', request_param='__formid__=login')
@view_config(attr='logout', request_param='__formid__=logout')
class AsyncAuthController(AuthController):
__view_mapper__ = AsyncFormViewMapper
@view_defaults(accept='text/html', renderer='templates/auth.pt')
@view_config(attr='forgot_password', route_name='forgot_password')
@view_config(attr='reset_password', route_name='reset_password')
class ForgotPasswordController(views.ForgotPasswordController):
pass
@view_defaults(accept='application/json', name='app', renderer='json')
@view_config(attr='forgot_password', request_param='__formid__=forgot')
class AsyncForgotPasswordController(ForgotPasswordController):
__view_mapper__ = AsyncFormViewMapper
@view_defaults(accept='text/html', renderer='templates/auth.pt')
@view_config(attr='register', route_name='register')
@view_config(attr='activate', route_name='activate')
class RegisterController(views.RegisterController):
pass
@view_defaults(accept='application/json', name='app', renderer='json')
@view_config(attr='register', request_param='__formid__=register')
@view_config(attr='activate', request_param='__formid__=activate')
class AsyncRegisterController(RegisterController):
__view_mapper__ = AsyncFormViewMapper
def activate(self):
"""Activate a user and set a password given an activation code.
This view is different from the activation view in horus because it
does not require the user id to be passed. It trusts the activation
code and updates the password.
"""
request = self.request
Str = self.Str
schema = request.registry.getUtility(interfaces.IActivateSchema)
schema = schema().bind(request=request)
form = request.registry.getUtility(interfaces.IActivateForm)(schema)
appstruct = None
try:
appstruct = form.validate(request.POST.items())
except deform.ValidationFailure as e:
return dict(errors=e.error.children)
code = appstruct['code']
activation = self.Activation.get_by_code(request, code)
user = None
if activation:
user = self.User.get_by_activation(request, activation)
if user is None:
return dict(errors=[_('This activation code is not valid.')])
user.password = appstruct['password']
self.db.delete(activation)
self.db.add(user)
FlashMessage(request, Str.reset_password_done, kind='success')
# XXX: Horus should maybe do this for us
event = events.RegistrationActivatedEvent(request, user, activation)
request.registry.notify(event)
return {}
@view_config(
context='h.interfaces.IStreamResource',
layout='stream',
@@ -283,13 +128,8 @@ def includeme(config):
config.include('pyramid_chameleon')
config.include('h.assets')
config.include('h.forms')
config.include('h.layouts')
config.include('h.panels')
config.include('h.schemas')
config.include('h.subscribers')
config.include('horus')
config.add_route('index', '/')
config.add_route('help', '/docs/help')
View
@@ -1,13 +1,6 @@
[app:main]
use: egg:h
# Authentication
#
# If authentications must be shared across servers or persisted across server
# reloads then this value should be set to a random 64-character string.
# Keep this value secure!
#auth.secret=
# API configuration
#
# Customize the key or leave it as the default. If the key is present without
@@ -39,6 +32,7 @@ api.key: 00000000-0000-0000-0000-000000000000
#es.index: annotator
# User and group framework settings -- see horus documentation
# Used by the local authentication provider
horus.login_redirect: /
horus.logout_redirect: /
#horus.activate_redirect: /
@@ -48,7 +42,7 @@ horus.logout_redirect: /
#horus.require_activation: True
# Authorization settings -- see pyramid_multiauth documentation
multiauth.policies: h.api h.models
multiauth.policies: h.auth.local
# Mail server configuration -- see the pyramid_mailer documentation
mail.default_sender: "Annotation Daemon" <no-reply@localhost>
View
@@ -44,6 +44,7 @@ def run_tests(self):
'horus>=0.9.15',
'jsonpointer==1.0',
'jsonschema==1.3.0',
'oauthlib>=0.6.1',
'pyramid>=1.5',
'pyramid-basemodel>=0.2',
'pyramid_deform>=0.2',
View
@@ -39,5 +39,5 @@ def wipe(settings):
with testConfig(settings=settings) as config:
authz = authorization.ACLAuthorizationPolicy()
config.set_authorization_policy(authz)
config.include('h.models')
config.include('h.api.store')
config.include('h.auth.local.models')
View

This file was deleted.

Oops, something went wrong.
View
@@ -38,7 +38,7 @@ def test_passive_queries():
event = events.AnnotationEvent(request, annotation, 'create')
with patch('h.notifier.AnnotationNotifier') as mock_notif:
with patch('h.models.UserSubscriptions.get_all') \
with patch('h.auth.local.models.UserSubscriptions.get_all') \
as mock_subscription:
query = QueryMock()
mock_subscription.all = Mock(return_value=[query])
@@ -57,12 +57,12 @@ def test_query_matches():
request = DummyRequest()
event = events.AnnotationEvent(request, annotation, 'create')
with patch('h.notifier.AnnotationNotifier') as mock_notif:
with patch('h.models.UserSubscriptions') as mock_subscription:
with patch('h.auth.local.models.UserSubscriptions') as mock_subs:
with patch('h.notifier.FilterHandler') as mock_filter:
query = QueryMock(active=True)
als = Mock()
als.all = Mock(return_value=[query])
mock_subscription.get_all = Mock(return_value=als)
mock_subs.get_all = Mock(return_value=als)
mock_filter().match = Mock(return_value=True)
notifier.send_notifications(event)
@@ -81,12 +81,12 @@ def test_query_mismatch():
request = DummyRequest()
event = events.AnnotationEvent(request, annotation, 'create')
with patch('h.notifier.AnnotationNotifier') as mock_notif:
with patch('h.models.UserSubscriptions') as mock_subscription:
with patch('h.auth.local.models.UserSubscriptions') as mock_subs:
with patch('h.notifier.FilterHandler') as mock_filter:
query = QueryMock(active=True)
als = Mock()
als.all = Mock(return_value=[query])
mock_subscription.get_all = Mock(return_value=als)
mock_subs.get_all = Mock(return_value=als)
mock_filter().match = Mock(return_value=False)
notifier.send_notifications(event)