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

Commit

Permalink
Reorganized code into multiple blueprints.
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Mar 19, 2013
1 parent f34cf78 commit 07f0517
Show file tree
Hide file tree
Showing 82 changed files with 1,453 additions and 1,124 deletions.
8 changes: 4 additions & 4 deletions config.rb
@@ -1,10 +1,10 @@
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "lastuserapp/static/css"
sass_dir = "lastuserapp/static/sass"
images_dir = "lastuserapp/static/img"
javascripts_dir = "lastuserapp/static/js"
css_dir = "lastuser_oauth/static/css"
sass_dir = "lastuser_oauth/static/sass"
images_dir = "lastuser_oauth/static/img"
javascripts_dir = "lastuser_oauth/static/js"
line_comments = false
# To enable relative paths to assets via compass helper functions. Uncomment:
relative_assets = true
19 changes: 19 additions & 0 deletions instance/settings-sample.py
Expand Up @@ -22,6 +22,25 @@
#: Timezone
TIMEZONE = 'Asia/Calcutta'

#: Reserved usernames
#: Add to this list but do not remove any unless you want to break
#: the website
RESERVED_USERNAMES = set([
'app',
'apps',
'auth',
'client',
'confirm',
'login',
'logout',
'new',
'profile',
'reset',
'register',
'token',
'organizations',
])

#: Mail settings
#: MAIL_FAIL_SILENTLY : default True
#: MAIL_SERVER : default 'localhost'
Expand Down
10 changes: 10 additions & 0 deletions lastuser_core/__init__.py
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-

from flask import Blueprint
from lastuser_core.registry import ResourceRegistry, LoginProviderRegistry

lastuser_core = Blueprint('lastuser_core', __name__)

#: Global resource registry
resource_registry = ResourceRegistry()
login_registry = LoginProviderRegistry()
69 changes: 69 additions & 0 deletions lastuser_core/models/__init__.py
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-

from inspect import isclass
from flask.ext.sqlalchemy import SQLAlchemy
from coaster.sqlalchemy import TimestampMixin, BaseMixin # Imported from here by other models

db = SQLAlchemy()


from lastuser_core.models.user import *
from lastuser_core.models.client import *
from lastuser_core.models.notice import *


def getuser(name):
if '@' in name:
# TODO: This should be handled by the LoginProvider registry, not here
if name.startswith('@'):
extid = UserExternalId.query.filter_by(service='twitter', username=name[1:]).first()
if extid:
return extid.user
else:
return None
else:
useremail = UserEmail.query.filter(UserEmail.email.in_([name, name.lower()])).first()
if useremail:
return useremail.user
# No verified email id. Look for an unverified id; return first found
useremail = UserEmailClaim.query.filter(UserEmailClaim.email.in_([name, name.lower()])).first()
if useremail:
return useremail.user
return None
else:
return User.query.filter_by(username=name).first()


def getextid(service, userid):
return UserExternalId.query.filter_by(service=service, userid=userid).first()


def merge_users(user1, user2):
"""
Merge two user accounts and return the new user account.
"""
# Always keep the older account and merge from the newer account
if user1.created_at < user2.created_at:
keep_user, merge_user = user1, user2
else:
keep_user, merge_user = user2, user1

# 1. Inspect all tables for foreign key references to merge_user and switch to keep_user.
for model in globals().values():
if isclass(model) and issubclass(model, db.Model) and model != User:
# a. This is a model and it's not the User model. Does it have a migrate_user classmethod?
if hasattr(model, 'migrate_user'):
model.migrate_user(olduser=merge_user, newuser=keep_user)
# b. No migrate_user? Does it have a user_id column?
elif hasattr(model, 'user_id') and hasattr(model, 'query'):
for row in model.query.filter_by(user_id=merge_user.id).all():
row.user_id = keep_user.id
# 2. Add merge_user's userid to olduserids. Commit session.
db.session.add(UserOldId(user=keep_user, userid=merge_user.userid))
db.session.commit()
# 3. Delete merge_user. Commit session.
db.session.delete(merge_user)
db.session.commit()

# 4. Return keep_user.
return keep_user
47 changes: 43 additions & 4 deletions lastuserapp/models/client.py → lastuser_core/models/client.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from coaster import newid, newsecret

from lastuserapp.models import db, BaseMixin
from lastuserapp.models.user import User, Organization, Team
from lastuser_core.models import db, BaseMixin
from lastuser_core.models.user import User, Organization, Team


class Client(BaseMixin, db.Model):
Expand Down Expand Up @@ -218,7 +218,7 @@ def scope(self):

@scope.setter
def scope(self, value):
self._scope = u' '.join(value)
self._scope = u' '.join(sorted(value))

scope = db.synonym('_scope', descriptor=scope)

Expand All @@ -243,6 +243,28 @@ def algorithm(self, value):

algorithm = db.synonym('_algorithm', descriptor=algorithm)

@classmethod
def migrate_user(cls, olduser, newuser):
if not olduser or not newuser:
return # Don't mess with client-only tokens
oldtokens = cls.query.filter_by(user=olduser).all()
newtokens = {} # Client: token mapping
for token in cls.query.filter_by(user=newuser).all():
newtokens.setdefault(token.client_id, []).append(token)

for token in oldtokens:
merge_performed = False
for newtoken in newtokens[token.client_id]:
if newtoken.user == newuser:
# There's another token for newuser with the same client.
# Just extend the scope there
newtoken.scope = set(newtoken.scope) | set(token.scope)
db.session.delete(token)
merge_performed = True
break
if merge_performed == False:
token.user = newuser # Reassign this token to newuser


class Permission(BaseMixin, db.Model):
__tablename__ = 'permission'
Expand All @@ -264,7 +286,7 @@ class Permission(BaseMixin, db.Model):
allusers = db.Column(db.Boolean, default=False, nullable=False)

def owner_is(self, user):
return self.user == user or (self.org and self.org in user.organizations_owned())
return user is not None and self.user == user or (self.org and self.org in user.organizations_owned())

def owner_name(self):
if self.user:
Expand Down Expand Up @@ -305,6 +327,23 @@ def pickername(self):
def userid(self):
return self.user.userid

@classmethod
def migrate_user(cls, olduser, newuser):
for operm in olduser.client_permissions:
merge_performed = False
for nperm in newuser.client_permissions:
if nperm.client == operm.client:
# Merge permission strings
tokens = set(operm.access_permissions.split(' '))
tokens.update(set(nperm.access_permissions.split(' ')))
if u' ' in tokens:
tokens.remove(u' ')
nperm.access_permissions = u' '.join(sorted(tokens))
db.session.delete(operm)
merge_performed = True
if not merge_performed:
operm.user = newuser


# This model's name is in plural because it defines multiple permissions within each instance
class TeamClientPermissions(BaseMixin, db.Model):
Expand Down
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

from lastuserapp.models import db, BaseMixin
from lastuser_core.models import db, BaseMixin

__all__ = ['SMSMessage', 'SMS_STATUS']

Expand Down
21 changes: 18 additions & 3 deletions lastuserapp/models/user.py → lastuser_core/models/user.py
Expand Up @@ -7,10 +7,10 @@
from flask import url_for
from coaster import newid, newsecret, newpin

from lastuserapp.models import db, BaseMixin
from lastuser_core.models import db, TimestampMixin, BaseMixin

__all__ = ['User', 'UserEmail', 'UserEmailClaim', 'PasswordResetRequest', 'UserExternalId',
'UserPhone', 'UserPhoneClaim', 'Team', 'Organization']
'UserPhone', 'UserPhoneClaim', 'Team', 'Organization', 'UserOldId']


class User(BaseMixin, db.Model):
Expand Down Expand Up @@ -157,6 +157,14 @@ def profile_url(self):
return url_for('profile')


class UserOldId(TimestampMixin, db.Model):
__tablename__ = 'useroldid'
userid = db.Column(db.String(22), nullable=False, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user = db.relationship(User, primaryjoin=user_id == User.id,
backref=db.backref('oldids', cascade="all, delete-orphan"))


class UserEmail(BaseMixin, db.Model):
__tablename__ = 'useremail'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
Expand Down Expand Up @@ -378,7 +386,7 @@ def clients_with_team_access(self):
"""
Return a list of clients with access to the organization's teams.
"""
from lastuserapp.models.client import CLIENT_TEAM_ACCESS
from lastuser_core.models.client import CLIENT_TEAM_ACCESS
return [cta.client for cta in self.client_team_access if cta.access_level == CLIENT_TEAM_ACCESS.ALL]

def permissions(self, user, inherited=None):
Expand Down Expand Up @@ -425,3 +433,10 @@ def permissions(self, user, inherited=None):
perms.add('edit')
perms.add('delete')
return perms

@classmethod
def migrate_user(cls, olduser, newuser):
for team in olduser.teams:
if team not in newuser.teams:
newuser.teams.append(team)
olduser.teams = []
64 changes: 54 additions & 10 deletions lastuserapp/registry.py → lastuser_core/registry.py
Expand Up @@ -11,7 +11,7 @@
except ImportError:
from ordereddict import OrderedDict
from flask import Response, request, jsonify, abort
from lastuserapp.models import AuthToken
from lastuser_core.models import AuthToken

# Bearer token, as per http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-15#section-2.1
auth_bearer_re = re.compile("^Bearer ([a-zA-Z0-9_.~+/-]+=*)$")
Expand Down Expand Up @@ -80,6 +80,21 @@ def decorated_function():
return wrapper


class LoginError(Exception):
"""External service login failure"""
pass


class LoginInitError(Exception):
"""External service login failure (during init)"""
pass


class LoginCallbackError(Exception):
"""External service login failure (during callback)"""
pass


class LoginProvider(object):
"""
Base class for login providers. Each implementation provides
Expand All @@ -91,26 +106,55 @@ class LoginProvider(object):
view and have full access to the view infrastructure. However, while
:meth:`do` is expected to return a Response to the user,
:meth:`callback` only returns information on the user back to Lastuser.
Implementations must take their configuration via the __init__
constructor.
:param name: Name of the service (stored in the database)
:param title: Title (shown to user)
:param at_login: (default True). Is this service available to the user for login? If false, it
will only be available to be added in the user's profile page. Use this for multiple instances
of the same external service with differing access permissions (for example, with Twitter).
:param priority: (default False). Is this service high priority? If False, it'll be hidden behind
a show more link.
"""

name = None
title = None
#: URL to icon for the login button
icon = None
#: Login form, if required
form = None

def do(self, **kwargs):
def __init__(self, name, title, at_login=True, priority=False, **kwargs):
self.name = name
self.title = title
self.at_login = at_login

def get_form(self):
"""
Returns form data, with three keys, next, error and form.
"""
return {'next': None, 'error': None, 'form': None}

def do(self, callback_url, form=None):
raise NotImplementedError

def callback(self, **kwargs):
def callback(self, *args, **kwargs):
raise NotImplementedError
return {
'userid': None, # Unique user id at this service
'username': None, # Public username. This may change
'avatar_url': None, # URL to avatar image
'oauth_token': None, # OAuth token, for OAuth-based services
'oauth_token_secret': None, # If required
'oauth_token_type': None, # Type of token
'email': None, # Verified email address. Service can be trusted
'emailclaim': None, # Claimed email address. Must be verified
'email_md5sum': None, # For when we have the email md5sum, but not the email itself
}


class LoginProviderRegistry(OrderedDict):
"""
Dictionary of login providers.
Dictionary of login providers (service: instance).
"""
pass

#: Global resource registry
resource_registry = ResourceRegistry()
login_registry = LoginProviderRegistry()
File renamed without changes.
16 changes: 16 additions & 0 deletions lastuser_oauth/__init__.py
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-

from flask import Blueprint
from flask.ext.assets import Bundle


lastuser_oauth = Blueprint('lastuser_oauth', __name__,
static_folder='static',
static_url_path='/static/oauth',
template_folder='templates')


lastuser_oauth_js = Bundle('lastuser_oauth/js/app.js')
lastuser_oauth_css = Bundle('lastuser_oauth/css/app.css')

from . import forms, views
5 changes: 5 additions & 0 deletions lastuser_oauth/forms/__init__.py
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-

from .login import *
from .profile import *
from .auth import *
8 changes: 8 additions & 0 deletions lastuser_oauth/forms/auth.py
@@ -0,0 +1,8 @@
from baseframe.forms import Form


class AuthorizeForm(Form):
"""
OAuth authorization form. Has no fields and is only used for CSRF protection.
"""
pass

0 comments on commit 07f0517

Please sign in to comment.