From 3d89227752a1047bdc328aafa513c7fc8db01c8d Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 9 Oct 2013 23:00:34 -0400 Subject: [PATCH] First iteration of sign-in with gihub. --- config.py | 21 ++++++++++++--- data/database.py | 25 +++++++++++++++-- data/model.py | 28 +++++++++++++++++++ endpoints/web.py | 62 ++++++++++++++++++++++++++++++++++++++++--- static/css/signin.css | 17 ++++++++++++ templates/signin.html | 8 ++++++ 6 files changed, 153 insertions(+), 8 deletions(-) diff --git a/config.py b/config.py index 03f58bb4f..9189c0727 100644 --- a/config.py +++ b/config.py @@ -72,8 +72,21 @@ class MixpanelProdConfig(object): MIXPANEL_KEY = '50ff2b2569faa3a51c8f5724922ffb7e' +class GitHubTestConfig(object): + GITHUB_CLIENT_ID = 'cfbc4aca88e5c1b40679' + GITHUB_CLIENT_SECRET = '7d1cc21e17e10cd8168410e2cd1e4561cb854ff9' + GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token' + GITHUB_USER_URL = 'https://api.github.com/user' + GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails' + + +class GitHubProdConfig(GitHubTestConfig): + GITHUB_CLIENT_ID = '5a8c08b06c48d89d4d1e' + GITHUB_CLIENT_SECRET = 'f89d8bb28ea3bd4e1c68808500d185a816be53b1' + + class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB, - StripeTestConfig, MixpanelTestConfig): + StripeTestConfig, MixpanelTestConfig, GitHubTestConfig): REGISTRY_SERVER = 'localhost:5000' LOGGING_CONFIG = { 'level': logging.DEBUG, @@ -83,7 +96,8 @@ class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB, class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, - StripeLiveConfig, MixpanelTestConfig): + StripeLiveConfig, MixpanelTestConfig, + GitHubProdConfig): REGISTRY_SERVER = 'localhost:5000' LOGGING_CONFIG = { 'level': logging.DEBUG, @@ -93,7 +107,8 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, - StripeLiveConfig, MixpanelProdConfig): + StripeLiveConfig, MixpanelProdConfig, + GitHubProdConfig): REGISTRY_SERVER = 'quay.io' LOGGING_CONFIG = { 'stream': sys.stderr, diff --git a/data/database.py b/data/database.py index c5b5e7e88..d82ecbae1 100644 --- a/data/database.py +++ b/data/database.py @@ -36,12 +36,32 @@ class BaseModel(Model): class User(BaseModel): username = CharField(unique=True, index=True) - password_hash = CharField() + password_hash = CharField(null=True) email = CharField(unique=True, index=True) verified = BooleanField(default=False) stripe_id = CharField(index=True, null=True) +class LoginService(BaseModel): + name = CharField(unique=True, index=True) + + +class FederatedLogin(BaseModel): + user = ForeignKeyField(User, index=True) + service = ForeignKeyField(LoginService, index=True) + service_ident = CharField() + + class Meta: + database = db + indexes = ( + # create a unique index on service and the local service id + (('service', 'service_ident'), True), + + # a user may only have one federated login per service + (('service', 'user'), True), + ) + + class Visibility(BaseModel): name = CharField(index=True) @@ -136,9 +156,10 @@ class RepositoryTag(BaseModel): def initialize_db(): create_model_tables([User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, RepositoryTag, - EmailConfirmation]) + EmailConfirmation, FederatedLogin, LoginService]) Role.create(name='admin') Role.create(name='write') Role.create(name='read') Visibility.create(name='public') Visibility.create(name='private') + LoginService.create(name='github') diff --git a/data/model.py b/data/model.py index bf59fcf69..c0fab9d21 100644 --- a/data/model.py +++ b/data/model.py @@ -34,6 +34,34 @@ def create_user(username, password, email): raise DataModelException(ex.message) +def create_federated_user(username, email, service_name, service_id): + try: + new_user = User.create(username=username, email=email, verified=True) + service = LoginService.get(LoginService.name == service_name) + federated_user = FederatedLogin.create(user=new_user, service=service, + service_ident=service_id) + + return new_user + + except Exception as ex: + raise DataModelException(ex.message) + + +def verify_federated_login(service_name, service_id): + selected = FederatedLogin.select(FederatedLogin, User) + with_service = selected.join(LoginService) + with_user = with_service.switch(FederatedLogin).join(User) + found = with_user.where(FederatedLogin.service_ident == service_id, + LoginService.name == service_name) + + found_list = list(found) + + if found_list: + return found_list[0].user + + return None + + def create_confirm_email_code(user): code = EmailConfirmation.create(user=user, email_confirm=True) return code diff --git a/endpoints/web.py b/endpoints/web.py index aaa3e560e..f253264b5 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -1,4 +1,5 @@ import logging +import requests from flask import (abort, send_file, redirect, request, url_for, render_template, make_response) @@ -66,7 +67,8 @@ def common_login(db_user): @app.route('/signin', methods=['GET']) def render_signin_page(): - return render_template('signin.html') + return render_template('signin.html', + github_client_id=app.config['GITHUB_CLIENT_ID']) @app.route('/signin', methods=['POST']) @@ -81,12 +83,66 @@ def signin(): return redirect(request.args.get('next') or url_for('index')) else: return render_template('signin.html', - needs_email_verification=True) + needs_email_verification=True, + github_client_id=app.config['GITHUB_CLIENT_ID']) else: return render_template('signin.html', username=username, - invalid_credentials=True) + invalid_credentials=True, + github_client_id=app.config['GITHUB_CLIENT_ID']) + + +@app.route('/oauth2/github/callback', methods=['GET']) +def github_oauth_callback(): + code = request.args.get('code') + payload = { + 'client_id': app.config['GITHUB_CLIENT_ID'], + 'client_secret': app.config['GITHUB_CLIENT_SECRET'], + 'code': code, + } + headers = { + 'Accept': 'application/json' + } + + get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'], + params=payload, headers=headers) + + token = get_access_token.json()['access_token'] + + token_param = { + 'access_token': token, + } + get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param) + + user_data = get_user.json() + username = user_data['login'] + github_id = user_data['id'] + + v3_media_type = { + 'Accept': 'application/vnd.github.v3' + } + get_email = requests.get(app.config['GITHUB_USER_EMAILS'], + params=token_param, headers=v3_media_type) + + # We will accept any email, but we prefer the primary + found_email = None + for user_email in get_email.json(): + found_email = user_email['email'] + if user_email['primary']: + break + + to_login = model.verify_federated_login('github', github_id) + if not to_login: + # try to create the user + to_login = model.create_federated_user(username, found_email, 'github', + github_id) + + if common_login(to_login): + return redirect(url_for('index')) + + # TODO something bad happened, we need to tell the user somehow + return redirect(url_for('signin')) @app.route('/confirm', methods=['GET']) diff --git a/static/css/signin.css b/static/css/signin.css index 637ffdfb5..f67dc78dd 100644 --- a/static/css/signin.css +++ b/static/css/signin.css @@ -8,6 +8,7 @@ body { max-width: 330px; padding: 15px; margin: 0 auto; + text-align: center; } .form-signin .form-signin-heading, .form-signin .checkbox { @@ -42,4 +43,20 @@ body { .alert { max-width: 300px; margin: 0 auto; +} + +.social-alternate { + color: #777; + font-size: 3em; + margin-left: 43px; +} + +.social-alternate .inner-text { + text-align: center; + position: relative; + color: white; + left: -43px; + top: -9px; + font-weight: bold; + font-size: .4em; } \ No newline at end of file diff --git a/templates/signin.html b/templates/signin.html index f6f180253..85c63940c 100644 --- a/templates/signin.html +++ b/templates/signin.html @@ -4,6 +4,7 @@ Sign In - Quay + @@ -13,6 +14,13 @@ + + + + OR + + + Sign In with GitHub {% if invalid_credentials %}