Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/local_settings_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from os import path

GITHUB_CONFIG = {
'repositories': [
('pretenders', 'deploystream'),
Expand All @@ -19,3 +21,7 @@

GIT_CONFIG = {
}

CSRF_ENABLED = False
TEST_DB_LOCATION = path.join(path.abspath(path.dirname(__file__)), 'test.db')
SQLALCHEMY_DATABASE_URI = ('sqlite:///' + TEST_DB_LOCATION)
20 changes: 18 additions & 2 deletions config/settings.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
from os import path, environ
import traceback

_basedir = path.abspath(path.dirname(__file__))

APP_PACKAGE = path.basename(path.dirname(__file__))
APP_PACKAGE = path.basename(_basedir)

# Default settings, overridden by the python file pointed to by CONFIG variable
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + path.join(_basedir, 'app.db')
DATABASE_CONNECT_OPTIONS = {}

# THREADS_PER_PAGE = 8

CSRF_ENABLED = True
CSRF_SESSION_KEY = "somethingimpossibletoguess"

# RECAPTCHA_USE_SSL = False
# RECAPTCHA_PUBLIC_KEY = 'blahblahblahblahblahblahblahblahblah'
# RECAPTCHA_PRIVATE_KEY = 'blahblahblahblahblahblahprivate'
# RECAPTCHA_OPTIONS = {'theme': 'white'}

GITHUB_CONFIG = GIT_CONFIG = SPRINTLY_CONFIG = JIRA_CONFIG = None

# The following is the programmatic equivalent of
# from deploystream.local_settings_<CONFIG> import *
GITHUB_CONFIG = GIT_CONFIG = SPRINTLY_CONFIG = JIRA_CONFIG = None

try:
CONFIG = environ.get('CONFIG', 'sample')
Expand Down
5 changes: 5 additions & 0 deletions deploystream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from os import environ
from os.path import join, dirname
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

APP_DIR = dirname(__file__)
CONFIG_DIR = join(dirname(APP_DIR), 'config')
Expand Down Expand Up @@ -51,6 +52,7 @@
# something that is actually a secret
app.secret_key = 'mysecret'

db = SQLAlchemy(app)
# Initialise the providers.
from providers import init_providers
classes = init_providers(app.config['PROVIDERS'])
Expand All @@ -62,3 +64,6 @@
# Import any views we want to register here at the bottom of the file:
import deploystream.views # NOQA
import deploystream.apps.feature.views # NOQA

from deploystream.apps.users.views import mod as usersModule
app.register_blueprint(usersModule)
81 changes: 73 additions & 8 deletions deploystream/apps/oauth/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from flask import session, redirect, flash, request, url_for
from flask_oauth import OAuth

from deploystream import app
from deploystream import app, db
from deploystream.apps.oauth import get_token, set_token
from deploystream.apps.users.models import User, OAuth as UserOAuth
from deploystream.apps.users.lib import (load_user_to_session,
get_user_id_from_session)
from deploystream.providers.interfaces import class_implements, IOAuthProvider


Expand Down Expand Up @@ -62,18 +65,79 @@ def oauth_authorized(resp):

set_token(session, oauth_name, resp['access_token'])

if request.args.get('islogin'):
user = OAUTH_OBJECTS[oauth_name].get('/user')
username = user.data['login']
session['username'] = username
# If registering, add a User and UserOAuth and load to session.
# If Logging in, then just loading to session
# If linking, then just adding a userOauth.

current_user_id = get_user_id_from_session(session)
remote_user = OAUTH_OBJECTS[oauth_name].get('/user')
remote_user_id = remote_user.data['id']

user = get_or_create_user_oauth(current_user_id, remote_user_id,
oauth_name, remote_user.data['login'])

load_user_to_session(session, user)

return redirect(next_url)


@app.route('/login')
def login():
def get_or_create_user_oauth(user_id, service_user_id, service_name,
service_username):
"""
Get or create OAuth information and Users with the given information.

Handles a number of scenarios:
- No user id is known. (ie user is logged out)
- OAuth object exists: return the associated user.
- OAuth object doesn't exist: create it, along with a user and link
them
- User id is known (ie user is logged in)
- Create and link the OAuth data to the account.

:param user_id:
The id of the currently logged in user. Or ``None`` if logged out.
:param service_user_id:
The id of the user according to the external service.
:param service_name:
The name used internally to reference the external service.
:param service_username:
The username that the service knows this user by. This will be used to
create the account in deploystream if logging in for the first time.
"""
if not user_id:
# We're either logging in or registering
oauth_obj = UserOAuth.query.filter_by(service_user_id=service_user_id,
service=service_name).first()
if not oauth_obj:
# Create a user and an OAuth linked to it.
current_user = User(username=service_username)
oauth = UserOAuth(service_user_id=service_user_id,
service=service_name,
service_username=service_username)
oauth.user = current_user
db.session.add(current_user)
db.session.add(oauth)
db.session.commit()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit unclear about how we really want to handle user registration and linking external accounts / oauth tokens. In particular how much user management we want in the core "open-source" project (in particular, self-registration).

E.g. now, any non-registered user can log in via github Oauth, and have a user account created with no email. Whereas for self-registered users, email is a required field.

Up for a discussion elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it some more I'd be happy with username+password for non oauth accounts as opposed to email+password - although I'm not sure that's your concern.

Happy to have a chat about this.

user_id = current_user.id
else:
user_id = oauth_obj.user.id

else:
# We're linking the account
oauth = UserOAuth(service_user_id=service_user_id,
service=service_name,
service_username=service_username,
user_id=user_id)
db.session.add(oauth)
db.session.commit()

return user_id


@app.route('/oauth/<oauth_name>')
def link_up(oauth_name):
"Handler for calls to login via github."
return start_token_processing('github', islogin=True)
return start_token_processing(oauth_name, islogin=True)


def start_token_processing(oauth_name, islogin=None):
Expand All @@ -83,4 +147,5 @@ def start_token_processing(oauth_name, islogin=None):
oauth_name=oauth_name,
islogin=islogin,
_external=True)

return OAUTH_OBJECTS[oauth_name].authorize(callback=url)
Empty file.
22 changes: 22 additions & 0 deletions deploystream/apps/users/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# User role
ADMIN = 0
STAFF = 1
USER = 2
ROLE = {
ADMIN: 'admin',
STAFF: 'staff',
USER: 'user',
}

# user status
INACTIVE = 0
NEW = 1
ACTIVE = 2
STATUS = {
INACTIVE: 'inactive',
NEW: 'new',
ACTIVE: 'active',
}

#OAUTH keys supported:
OAUTHS = ['github']
13 changes: 13 additions & 0 deletions deploystream/apps/users/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from functools import wraps

from flask import g, flash, redirect, url_for, request
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

g... how less cryptical could they have made this? :P

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.



def requires_login(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
flash(u'You need to be signed in for this page.')
return redirect(url_for('users.login', next=request.path))
return f(*args, **kwargs)
return decorated_function
35 changes: 35 additions & 0 deletions deploystream/apps/users/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from flask.ext.wtf import (
Form, TextField, PasswordField, BooleanField, RecaptchaField
)
from flask.ext.wtf import Required, Email, EqualTo

from .models import User


class LoginForm(Form):
username = TextField('Username', [Required()])
password = PasswordField('Password', [Required()])


class RegisterForm(Form):
username = TextField('Username', [Required()])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am getting SQL Alchemy exceptions now.
What is the equivalent to migrations in this flask world?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I dropped and re-created but there is probably a better approach.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't used it, but I've seen this https://code.google.com/p/sqlalchemy-migrate/ exists.

Here's an example for using it with flask: http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iv-database

At the moment, I'm doing the same thing as you - deleting and restoring.

email = TextField('Email address', [Email()])
password = PasswordField('Password', [Required()])
confirm = PasswordField('Repeat Password', [
Required(),
EqualTo('password', message='Passwords must match')
])
accept_tos = BooleanField('I accept the TOS', [Required()])
#recaptcha = RecaptchaField()

def validate(self):
rv = Form.validate(self)
if not rv:
return False

uname_clash = User.query.filter_by(username=self.username.data).first()
if uname_clash:
self.username.errors.append("This username is already in use.")
return False

return True
10 changes: 10 additions & 0 deletions deploystream/apps/users/lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def load_user_to_session(session, user):
session['user_id'] = user.id
session['user_name'] = user.username


def get_user_id_from_session(session):
try:
return session['user_id']
except KeyError:
return None
48 changes: 48 additions & 0 deletions deploystream/apps/users/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from datetime import datetime

from deploystream import db
from deploystream.apps.users import constants as USERS


class User(db.Model):

__tablename__ = 'users_user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True)
email = db.Column(db.String(120))
password = db.Column(db.String(20))
role = db.Column(db.SmallInteger, default=USERS.USER)
status = db.Column(db.SmallInteger, default=USERS.NEW)
created = db.Column(db.DateTime, default=datetime.now)

oauth_keys = db.relationship('OAuth', backref='user')

def __init__(self, username=None, email=None, password=None):
self.username = username
self.email = email
self.password = password

def getStatus(self):
return USERS.STATUS[self.status]

def getRole(self):
return USERS.ROLE[self.role]

def __repr__(self):
return '<User %r>' % (self.username)


class OAuth(db.Model):

__tablename__ = 'users_oauth'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users_user.id'))
service = db.Column(db.String(20))
service_user_id = db.Column(db.String(120))
service_username = db.Column(db.String(50))

# A user on an external service should only exist once in deploystream.
db.UniqueConstraint('service', 'service_user_id', name='service_user')

# Each user in deploystream should only have each service oauth'd once.
db.UniqueConstraint('user_id', 'service', name='user_service')
27 changes: 27 additions & 0 deletions deploystream/apps/users/template_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from . import constants
from .models import OAuth
from deploystream import app


@app.template_filter('all_oauths')
def all_oauths(user):
"""Return all available oauths for the User.

Returns all oauth information including those that the user hasn't
connected to yet.
"""
all_oauth = []
for oauth_service in constants.OAUTHS:
user_oauth = OAuth.query.filter_by(user_id=user.id,
service=oauth_service).first()
if user_oauth:
all_oauth.append((oauth_service, user_oauth.service_username))
else:
all_oauth.append((oauth_service, None))

return all_oauth


@app.template_filter('humanize_time')
def humanize_time(datetime):
return datetime.strftime("%B %d, %Y")
Loading