Merge pull request #447 from coreos-inc/ronon
Add support for Dex to Quay
This commit is contained in:
commit
edef283697
27 changed files with 533 additions and 176 deletions
8
app.py
8
app.py
|
@ -26,7 +26,9 @@ from util import get_app_url
|
||||||
from util.saas.analytics import Analytics
|
from util.saas.analytics import Analytics
|
||||||
from util.saas.exceptionlog import Sentry
|
from util.saas.exceptionlog import Sentry
|
||||||
from util.names import urn_generator
|
from util.names import urn_generator
|
||||||
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
|
from util.config.oauth import (GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig,
|
||||||
|
DexOAuthConfig)
|
||||||
|
|
||||||
from util.security.signing import Signer
|
from util.security.signing import Signer
|
||||||
from util.saas.cloudwatch import start_cloudwatch_sender
|
from util.saas.cloudwatch import start_cloudwatch_sender
|
||||||
from util.saas.metricqueue import MetricQueue
|
from util.saas.metricqueue import MetricQueue
|
||||||
|
@ -135,7 +137,9 @@ github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG')
|
||||||
github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG')
|
github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG')
|
||||||
gitlab_trigger = GitLabOAuthConfig(app.config, 'GITLAB_TRIGGER_CONFIG')
|
gitlab_trigger = GitLabOAuthConfig(app.config, 'GITLAB_TRIGGER_CONFIG')
|
||||||
google_login = GoogleOAuthConfig(app.config, 'GOOGLE_LOGIN_CONFIG')
|
google_login = GoogleOAuthConfig(app.config, 'GOOGLE_LOGIN_CONFIG')
|
||||||
oauth_apps = [github_login, github_trigger, gitlab_trigger, google_login]
|
dex_login = DexOAuthConfig(app.config, 'DEX_LOGIN_CONFIG')
|
||||||
|
|
||||||
|
oauth_apps = [github_login, github_trigger, gitlab_trigger, google_login, dex_login]
|
||||||
|
|
||||||
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf)
|
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf)
|
||||||
image_replication_queue = WorkQueue(app.config['REPLICATION_QUEUE_NAME'], tf)
|
image_replication_queue = WorkQueue(app.config['REPLICATION_QUEUE_NAME'], tf)
|
||||||
|
|
|
@ -153,6 +153,9 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether Google login is supported.
|
# Feature Flag: Whether Google login is supported.
|
||||||
FEATURE_GOOGLE_LOGIN = False
|
FEATURE_GOOGLE_LOGIN = False
|
||||||
|
|
||||||
|
# Feature Flag: Whther Dex login is supported.
|
||||||
|
FEATURE_DEX_LOGIN = False
|
||||||
|
|
||||||
# Feature flag, whether to enable olark chat
|
# Feature flag, whether to enable olark chat
|
||||||
FEATURE_OLARK_CHAT = False
|
FEATURE_OLARK_CHAT = False
|
||||||
|
|
||||||
|
@ -184,6 +187,9 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether to automatically replicate between storage engines.
|
# Feature Flag: Whether to automatically replicate between storage engines.
|
||||||
FEATURE_STORAGE_REPLICATION = False
|
FEATURE_STORAGE_REPLICATION = False
|
||||||
|
|
||||||
|
# Feature Flag: Whether users can directly login to the UI.
|
||||||
|
FEATURE_DIRECT_LOGIN = True
|
||||||
|
|
||||||
BUILD_MANAGER = ('enterprise', {})
|
BUILD_MANAGER = ('enterprise', {})
|
||||||
|
|
||||||
DISTRIBUTED_STORAGE_CONFIG = {
|
DISTRIBUTED_STORAGE_CONFIG = {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Add support for Dex login
|
||||||
|
|
||||||
|
Revision ID: 3a3bb77e17d5
|
||||||
|
Revises: 9512773a4a2
|
||||||
|
Create Date: 2015-09-04 15:57:38.007822
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3a3bb77e17d5'
|
||||||
|
down_revision = '9512773a4a2'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.bulk_insert(tables.loginservice, [{'id': 7, 'name': 'dex'}])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.execute(
|
||||||
|
tables.loginservice.delete()
|
||||||
|
.where(tables.loginservice.c.name == op.inline_literal('dex'))
|
||||||
|
)
|
||||||
|
|
|
@ -306,6 +306,7 @@ class User(ApiResource):
|
||||||
return user_view(user)
|
return user_view(user)
|
||||||
|
|
||||||
@show_if(features.USER_CREATION)
|
@show_if(features.USER_CREATION)
|
||||||
|
@show_if(features.DIRECT_LOGIN)
|
||||||
@nickname('createNewUser')
|
@nickname('createNewUser')
|
||||||
@internal_only
|
@internal_only
|
||||||
@validate_json_request('NewUser')
|
@validate_json_request('NewUser')
|
||||||
|
@ -496,6 +497,7 @@ class ConvertToOrganization(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/signin')
|
@resource('/v1/signin')
|
||||||
|
@show_if(features.DIRECT_LOGIN)
|
||||||
@internal_only
|
@internal_only
|
||||||
class Signin(ApiResource):
|
class Signin(ApiResource):
|
||||||
""" Operations for signing in the user. """
|
""" Operations for signing in the user. """
|
||||||
|
@ -595,6 +597,7 @@ class Signout(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/detachexternal/<servicename>')
|
@resource('/v1/detachexternal/<servicename>')
|
||||||
|
@show_if(features.DIRECT_LOGIN)
|
||||||
@internal_only
|
@internal_only
|
||||||
class DetachExternal(ApiResource):
|
class DetachExternal(ApiResource):
|
||||||
""" Resource for detaching an external login. """
|
""" Resource for detaching an external login. """
|
||||||
|
|
|
@ -5,7 +5,7 @@ from flask import request, redirect, url_for, Blueprint
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
|
|
||||||
from endpoints.common import render_page_template, common_login, route_show_if
|
from endpoints.common import render_page_template, common_login, route_show_if
|
||||||
from app import app, analytics, get_app_url, github_login, google_login
|
from app import app, analytics, get_app_url, github_login, google_login, dex_login
|
||||||
from data import model
|
from data import model
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.validation import generate_valid_usernames
|
from util.validation import generate_valid_usernames
|
||||||
|
@ -14,6 +14,7 @@ from auth.auth import require_session_login
|
||||||
from peewee import IntegrityError
|
from peewee import IntegrityError
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
from util.security.strictjwt import decode, InvalidTokenError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
client = app.config['HTTPCLIENT']
|
client = app.config['HTTPCLIENT']
|
||||||
|
@ -24,7 +25,7 @@ def render_ologin_error(service_name,
|
||||||
return render_page_template('ologinerror.html', service_name=service_name,
|
return render_page_template('ologinerror.html', service_name=service_name,
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
service_url=get_app_url(),
|
service_url=get_app_url(),
|
||||||
user_creation=features.USER_CREATION)
|
user_creation=features.USER_CREATION and features.DIRECT_LOGIN)
|
||||||
|
|
||||||
|
|
||||||
def get_user(service, token):
|
def get_user(service, token):
|
||||||
|
@ -86,7 +87,7 @@ def conduct_oauth_login(service, user_id, username, email, metadata={}):
|
||||||
|
|
||||||
return render_ologin_error(service_name)
|
return render_ologin_error(service_name)
|
||||||
|
|
||||||
def get_google_username(user_data):
|
def get_email_username(user_data):
|
||||||
username = user_data['email']
|
username = user_data['email']
|
||||||
at = username.find('@')
|
at = username.find('@')
|
||||||
if at > 0:
|
if at > 0:
|
||||||
|
@ -108,7 +109,7 @@ def google_oauth_callback():
|
||||||
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
|
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
|
||||||
return render_ologin_error('Google')
|
return render_ologin_error('Google')
|
||||||
|
|
||||||
username = get_google_username(user_data)
|
username = get_email_username(user_data)
|
||||||
metadata = {
|
metadata = {
|
||||||
'service_username': user_data['email']
|
'service_username': user_data['email']
|
||||||
}
|
}
|
||||||
|
@ -194,7 +195,7 @@ def google_oauth_attach():
|
||||||
google_id = user_data['id']
|
google_id = user_data['id']
|
||||||
user_obj = current_user.db_user()
|
user_obj = current_user.db_user()
|
||||||
|
|
||||||
username = get_google_username(user_data)
|
username = get_email_username(user_data)
|
||||||
metadata = {
|
metadata = {
|
||||||
'service_username': user_data['email']
|
'service_username': user_data['email']
|
||||||
}
|
}
|
||||||
|
@ -236,3 +237,83 @@ def github_oauth_attach():
|
||||||
return render_ologin_error('GitHub', err)
|
return render_ologin_error('GitHub', err)
|
||||||
|
|
||||||
return redirect(url_for('web.user'))
|
return redirect(url_for('web.user'))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_user_jwt(token, oidc_provider):
|
||||||
|
try:
|
||||||
|
return decode(token, oidc_provider.get_public_key(), algorithms=['RS256'],
|
||||||
|
audience=oidc_provider.client_id(),
|
||||||
|
issuer=oidc_provider.issuer)
|
||||||
|
except InvalidTokenError:
|
||||||
|
# Public key may have expired. Try to retrieve an updated public key and use it to decode.
|
||||||
|
return decode(token, oidc_provider.get_public_key(force_refresh=True), algorithms=['RS256'],
|
||||||
|
audience=oidc_provider.client_id(),
|
||||||
|
issuer=oidc_provider.issuer)
|
||||||
|
|
||||||
|
|
||||||
|
@oauthlogin.route('/dex/callback', methods=['GET', 'POST'])
|
||||||
|
@route_show_if(features.DEX_LOGIN)
|
||||||
|
def dex_oauth_callback():
|
||||||
|
error = request.values.get('error', None)
|
||||||
|
if error:
|
||||||
|
return render_ologin_error(dex_login.public_title, error)
|
||||||
|
|
||||||
|
code = request.values.get('code')
|
||||||
|
if not code:
|
||||||
|
return render_ologin_error(dex_login.public_title, 'Missing OAuth code')
|
||||||
|
|
||||||
|
token = dex_login.exchange_code_for_token(app.config, client, code, client_auth=True,
|
||||||
|
form_encode=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = decode_user_jwt(token, dex_login)
|
||||||
|
except InvalidTokenError:
|
||||||
|
logger.exception('Exception when decoding returned JWT')
|
||||||
|
return render_ologin_error(dex_login.public_title,
|
||||||
|
'Could not decode response. Please contact your system administrator about this error.')
|
||||||
|
|
||||||
|
username = get_email_username(payload)
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
dex_id = payload['sub']
|
||||||
|
email_address = payload['email']
|
||||||
|
|
||||||
|
if not payload.get('email_verified', False):
|
||||||
|
return render_ologin_error(dex_login.public_title,
|
||||||
|
'A verified e-mail address is required for login. Please verify your ' +
|
||||||
|
'e-mail address in %s and try again.' % dex_login.public_title)
|
||||||
|
|
||||||
|
|
||||||
|
return conduct_oauth_login(dex_login, dex_id, username, email_address,
|
||||||
|
metadata=metadata)
|
||||||
|
|
||||||
|
|
||||||
|
@oauthlogin.route('/dex/callback/attach', methods=['GET', 'POST'])
|
||||||
|
@route_show_if(features.DEX_LOGIN)
|
||||||
|
@require_session_login
|
||||||
|
def dex_oauth_attach():
|
||||||
|
code = request.args.get('code')
|
||||||
|
token = dex_login.exchange_code_for_token(app.config, client, code, redirect_suffix='/attach',
|
||||||
|
client_auth=True, form_encode=True)
|
||||||
|
if not token:
|
||||||
|
return render_ologin_error(dex_login.public_title)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = decode_user_jwt(token, dex_login)
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
logger.exception('Exception when decoding returned JWT')
|
||||||
|
return render_ologin_error(dex_login.public_title,
|
||||||
|
'Could not decode response. Please contact your system administrator about this error.')
|
||||||
|
|
||||||
|
user_obj = current_user.db_user()
|
||||||
|
dex_id = payload['sub']
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
model.user.attach_federated_login(user_obj, 'dex', dex_id, metadata=metadata)
|
||||||
|
except IntegrityError:
|
||||||
|
err = '%s account is already attached to a %s account' % (dex_login.public_title,
|
||||||
|
app.config['REGISTRY_TITLE_SHORT'])
|
||||||
|
return render_ologin_error(dex_login.public_title, err)
|
||||||
|
|
||||||
|
return redirect(url_for('web.user'))
|
||||||
|
|
|
@ -218,6 +218,7 @@ def initialize_database():
|
||||||
LoginService.create(name='ldap')
|
LoginService.create(name='ldap')
|
||||||
LoginService.create(name='jwtauthn')
|
LoginService.create(name='jwtauthn')
|
||||||
LoginService.create(name='keystone')
|
LoginService.create(name='keystone')
|
||||||
|
LoginService.create(name='dex')
|
||||||
|
|
||||||
BuildTriggerService.create(name='github')
|
BuildTriggerService.create(name='github')
|
||||||
BuildTriggerService.create(name='custom-git')
|
BuildTriggerService.create(name='custom-git')
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
.external-login-button i.fa {
|
.external-login-button i.fa,
|
||||||
|
.external-login-button img {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
width: 24px;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
|
@ -6,6 +6,9 @@
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.external-logins-manager .external-auth-provider td:first-child i.fa {
|
.external-logins-manager .external-auth-provider-title i.fa,
|
||||||
|
.external-logins-manager .external-auth-provider-title img {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
|
@ -5,3 +5,13 @@
|
||||||
.signup-form-element .co-alert {
|
.signup-form-element .co-alert {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signup-form-element .single-sign-on a {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form-element .single-sign-on .external-login-button i.fa,
|
||||||
|
.signup-form-element .single-sign-on .external-login-button img {
|
||||||
|
width: 30px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
|
@ -1,24 +1,14 @@
|
||||||
<span class="external-login-button-element">
|
<span class="external-login-button-element">
|
||||||
<span ng-if="provider == 'github'">
|
<a href="javascript:void(0)" ng-class="isLink ? '' : 'btn btn-primary btn-block'"
|
||||||
<a href="javascript:void(0)" ng-class="isLink ? '' : 'btn btn-primary btn-block'" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px" ng-disabled="signingIn">
|
ng-if="providerInfo.enabled" ng-click="startSignin()" style="margin-bottom: 10px"
|
||||||
<i class="fa fa-github fa-lg"></i>
|
ng-disabled="signingIn">
|
||||||
<span ng-if="action != 'attach'">
|
<img ng-src="{{ providerInfo.icon().url }}" ng-if="providerInfo.icon().url">
|
||||||
Sign In with GitHub
|
<i class="fa" ng-class="providerInfo.icon().icon" ng-if="providerInfo.icon().icon"></i>
|
||||||
<span ng-if="isEnterprise('github')">Enterprise</span>
|
<span ng-if="action != 'attach'">
|
||||||
</span>
|
Sign In with {{ providerInfo.title() }}
|
||||||
<span ng-if="action == 'attach'">
|
</span>
|
||||||
Attach to GitHub
|
<span ng-if="action == 'attach'">
|
||||||
<span ng-if="isEnterprise('github')">Enterprise</span>
|
Attach to {{ providerInfo.title() }}
|
||||||
Account
|
</span>
|
||||||
</span>
|
</a>
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span ng-if="provider == 'google'">
|
|
||||||
<a href="javascript:void(0)" ng-class="isLink ? '' : 'btn btn-primary btn-block'" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')" ng-disabled="signingIn">
|
|
||||||
<i class="fa fa-google fa-lg"></i>
|
|
||||||
<span ng-if="action != 'attach'">Sign In with Google</span>
|
|
||||||
<span ng-if="action == 'attach'">Attach to Google Account</span>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -9,52 +9,35 @@
|
||||||
<thead>
|
<thead>
|
||||||
<td>Provider</td>
|
<td>Provider</td>
|
||||||
<td>Account Status</td>
|
<td>Account Status</td>
|
||||||
<td>Attach/Detach</td>
|
<td quay-show="Features.DIRECT_LOGIN">Attach/Detach</td>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<!-- GitHub Login -->
|
<tr class="external-auth-provider" ng-repeat="provider in EXTERNAL_LOGINS">
|
||||||
<tr class="external-auth-provider" ng-show="Features.GITHUB_LOGIN">
|
<td class="external-auth-provider-title">
|
||||||
<td>
|
<img ng-src="{{ provider.icon().url }}" ng-if="provider.icon().url">
|
||||||
<i class="fa fa-github"></i> GitHub <span ng-if="KeyService.isEnterprise('github')">Enterprise</span>
|
<i class="fa" ng-class="provider.icon().icon" ng-if="provider.icon().icon"></i>
|
||||||
|
{{ provider.title() }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span ng-if="hasGithubLogin">
|
<span ng-if="externalLoginInfo[provider.id]">
|
||||||
Attached to GitHub <span ng-if="KeyService.isEnterprise('github')">Enterprise</span> account <b><a href="{{githubEndpoint}}{{githubLogin}}" target="_blank">{{githubLogin}}</a></b>
|
Attached to {{ provider.title() }} account
|
||||||
|
<b ng-if="provider.hasUserInfo">
|
||||||
|
<a ng-href="{{ provider.getUserInfo(externalLoginInfo[provider.id]).endpoint }}" target="_blank">
|
||||||
|
{{ provider.getUserInfo(externalLoginInfo[provider.id]).username }}
|
||||||
|
</a>
|
||||||
|
</b>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="empty" ng-if="!hasGithubLogin">
|
<span class="empty" ng-if="!externalLoginInfo[provider.id]">
|
||||||
(Not attached to GitHub<span ng-if="KeyService.isEnterprise('github')"> Enterprise</span>)
|
Not attached to {{ provider.title() }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<span class="external-login-button" provider="github" action="attach" is-link="true"
|
<span class="external-login-button" provider="{{ provider.id }}" action="attach" is-link="true"
|
||||||
ng-if="!hasGithubLogin"></span>
|
ng-if="!externalLoginInfo[provider.id]"></span>
|
||||||
<a href="javascript:void(0)" ng-if="hasGithubLogin"
|
<a href="javascript:void(0)" ng-if="externalLoginInfo[provider.id] && Features.DIRECT_LOGIN"
|
||||||
ng-click="detachExternalLogin('github')">Detach Account</a>
|
ng-click="detachExternalLogin(provider.id)">Detach Account</a>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Google Login -->
|
|
||||||
<tr class="external-auth-provider" ng-show="Features.GOOGLE_LOGIN">
|
|
||||||
<td>
|
|
||||||
<i class="fa fa-google"></i> Google Account
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span ng-if="hasGoogleLogin">
|
|
||||||
Attached to Google account <b>{{ googleLogin }}</b>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="empty" ng-if="!hasGoogleLogin">
|
|
||||||
(Not attached to a Google account)
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span class="external-login-button" provider="google" action="attach" is-link="true"
|
|
||||||
ng-if="!hasGoogleLogin"></span>
|
|
||||||
<a href="javascript:void(0)" ng-if="hasGoogleLogin"
|
|
||||||
ng-click="detachExternalLogin('google')">Detach Account</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -42,7 +42,8 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li ng-switch-default>
|
<li ng-switch-default>
|
||||||
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
|
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}" ng-if="!externalSigninUrl">Sign in</a>
|
||||||
|
<a class="user-view" ng-href="{{ externalSigninUrl }}" ng-if="externalSigninUrl">Sign in</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -133,7 +134,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li ng-switch-default>
|
<li ng-switch-default>
|
||||||
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}">Sign in</a>
|
<a class="user-view" href="/signin/" target="{{ appLinkTarget() }}" ng-if="!externalSigninUrl">Sign in</a>
|
||||||
|
<a class="user-view" ng-href="{{ externalSigninUrl }}" ng-if="externalSigninUrl">Sign in</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div><!-- /.navbar-collapse -->
|
</div><!-- /.navbar-collapse -->
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
<div class="signin-form-element" style="position: relative">
|
<div class="signin-form-element" style="position: relative">
|
||||||
<span class="cor-loader" ng-show="signingIn"></span>
|
<span class="cor-loader" ng-show="signingIn"></span>
|
||||||
|
|
||||||
<form class="form-signin" ng-submit="signin();" ng-show="!signingIn">
|
<form class="form-signin" ng-submit="signin();" ng-show="!signingIn">
|
||||||
<input type="text" class="form-control input-lg" name="username"
|
<div quay-show="Features.DIRECT_LOGIN">
|
||||||
placeholder="Username or E-mail Address" ng-model="user.username" autofocus>
|
<input type="text" class="form-control input-lg" name="username"
|
||||||
<input type="password" class="form-control input-lg" name="password"
|
placeholder="Username or E-mail Address" ng-model="user.username" autofocus>
|
||||||
placeholder="Password" ng-model="user.password">
|
<input type="password" class="form-control input-lg" name="password"
|
||||||
|
placeholder="Password" ng-model="user.password">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="co-alert co-alert-warning" ng-show="tryAgainSoon > 0">
|
<div class="co-alert co-alert-warning" ng-show="tryAgainSoon > 0">
|
||||||
Too many attempts have been made to login. Please try again in {{ tryAgainSoon }} second<span ng-if="tryAgainSoon != 1">s</span>.
|
Too many attempts have been made to login. Please try again in {{ tryAgainSoon }} second<span ng-if="tryAgainSoon != 1">s</span>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span ng-show="tryAgainSoon == 0">
|
<span ng-show="tryAgainSoon == 0">
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit" quay-show="Features.DIRECT_LOGIN">Sign In</button>
|
||||||
|
|
||||||
<span class="social-alternate" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">
|
<span class="social-alternate" quay-show="EXTERNAL_LOGINS.length && Features.DIRECT_LOGIN">
|
||||||
<i class="fa fa-circle"></i>
|
<i class="fa fa-circle"></i>
|
||||||
<span class="inner-text">OR</span>
|
<span class="inner-text">OR</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="external-login-button" provider="github" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
|
<div class="external-login-button" provider="{{ provider.id }}" redirect-url="redirectUrl"
|
||||||
<div class="external-login-button" provider="google" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
|
sign-in-started="markStarted()" ng-repeat="provider in EXTERNAL_LOGINS"></div>
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,39 @@
|
||||||
<div class="signup-form-element"
|
<div class="signup-form-element">
|
||||||
quay-show="Features.USER_CREATION && Config.AUTHENTICATION_TYPE == 'Database'">
|
<div quay-show="singleSigninUrl" class="single-sign-on">
|
||||||
<form class="form-signup" name="signupForm" ng-submit="register()" ng-show="!awaitingConfirmation && !registering">
|
<div class="external-login-button" provider="{{ EXTERNAL_LOGINS[0].id }}"></div>
|
||||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
|
</div>
|
||||||
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
|
||||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
|
<div quay-show="Features.USER_CREATION && Config.AUTHENTICATION_TYPE == 'Database' && !singleSigninUrl">
|
||||||
ng-pattern="/^.{8,}$/">
|
<form class="form-signup" name="signupForm" ng-submit="register()" ng-show="!awaitingConfirmation && !registering">
|
||||||
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatPassword"
|
<div quay-show="Features.DIRECT_LOGIN">
|
||||||
match="newUser.password" required
|
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required ng-pattern="/^[a-z0-9_]{4,30}$/">
|
||||||
ng-pattern="/^.{8,}$/">
|
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
||||||
<div class="form-group signin-buttons">
|
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required
|
||||||
<button id="signupButton"
|
ng-pattern="/^.{8,}$/">
|
||||||
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatPassword"
|
||||||
analytics-on analytics-event="register">
|
match="newUser.password" required
|
||||||
<span quay-show="Features.BILLING">Sign Up for Free!</span>
|
ng-pattern="/^.{8,}$/">
|
||||||
<span quay-show="!Features.BILLING">Sign Up</span>
|
</div>
|
||||||
</button>
|
<div class="form-group signin-buttons">
|
||||||
<span class="social-alternate" quay-require="['GITHUB_LOGIN']">
|
<button id="signupButton"
|
||||||
<i class="fa fa-circle"></i>
|
class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit"
|
||||||
<span class="inner-text">OR</span>
|
analytics-on analytics-event="register"
|
||||||
</span>
|
quay-show="Features.DIRECT_LOGIN">
|
||||||
<div class="external-login-button" provider="github"></div>
|
<span quay-show="Features.BILLING">Sign Up for Free!</span>
|
||||||
<div class="external-login-button" provider="google"></div>
|
<span quay-show="!Features.BILLING">Sign Up</span>
|
||||||
|
</button>
|
||||||
|
<span class="social-alternate" quay-show="Features.DIRECT_LOGIN && EXTERNAL_LOGINS.length">
|
||||||
|
<i class="fa fa-circle"></i>
|
||||||
|
<span class="inner-text">OR</span>
|
||||||
|
</span>
|
||||||
|
<div class="external-login-button" provider="{{ provider.id }}" ng-repeat="provider in EXTERNAL_LOGINS"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="cor-loader" ng-show="registering"></div>
|
||||||
|
<div class="co-alert co-alert-info"
|
||||||
|
ng-show="awaitingConfirmation && hideRegisteredMessage != 'true'">
|
||||||
|
Thank you for registering! We have sent you an activation email.
|
||||||
|
You must <b>verify your email address</b> before you can continue.
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<div class="cor-loader" ng-show="registering"></div>
|
|
||||||
<div class="co-alert co-alert-info"
|
|
||||||
ng-show="awaitingConfirmation && hideRegisteredMessage != 'true'">
|
|
||||||
Thank you for registering! We have sent you an activation email.
|
|
||||||
You must <b>verify your email address</b> before you can continue.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default"
|
<div class="panel panel-default"
|
||||||
quay-show="Features.USER_CREATION && Config.AUTHENTICATION_TYPE == 'Database'">
|
quay-show="Features.USER_CREATION && Config.AUTHENTICATION_TYPE == 'Database' && Features.DIRECT_LOGIN">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h6 class="panel-title accordion-title">
|
<h6 class="panel-title accordion-title">
|
||||||
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseRegister">
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseRegister">
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default"
|
<div class="panel panel-default"
|
||||||
quay-show="Features.MAILING && Config.AUTHENTICATION_TYPE == 'Database'">
|
quay-show="Features.MAILING && Config.AUTHENTICATION_TYPE == 'Database' && Features.DIRECT_LOGIN">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h6 class="panel-title accordion-title">
|
<h6 class="panel-title accordion-title">
|
||||||
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseForgot">
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseForgot">
|
||||||
|
|
|
@ -15,14 +15,14 @@ angular.module('quay').directive('externalLoginButton', function () {
|
||||||
'provider': '@provider',
|
'provider': '@provider',
|
||||||
'action': '@action'
|
'action': '@action'
|
||||||
},
|
},
|
||||||
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
|
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, ExternalLoginService) {
|
||||||
$scope.signingIn = false;
|
$scope.signingIn = false;
|
||||||
$scope.isEnterprise = KeyService.isEnterprise;
|
$scope.providerInfo = ExternalLoginService.getProvider($scope.provider);
|
||||||
|
|
||||||
$scope.startSignin = function(service) {
|
$scope.startSignin = function() {
|
||||||
$scope.signInStarted({'service': service});
|
$scope.signInStarted({'service': $scope.provider});
|
||||||
|
|
||||||
var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login');
|
var url = ExternalLoginService.getLoginUrl($scope.provider, $scope.action || 'login');
|
||||||
|
|
||||||
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
|
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
|
||||||
var redirectURL = $scope.redirectUrl || window.location.toString();
|
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||||
|
|
|
@ -11,41 +11,37 @@ angular.module('quay').directive('externalLoginsManager', function () {
|
||||||
scope: {
|
scope: {
|
||||||
'user': '=user',
|
'user': '=user',
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, ApiService, UserService, Features, Config, KeyService) {
|
controller: function($scope, $element, ApiService, UserService, Features, Config, KeyService,
|
||||||
|
ExternalLoginService) {
|
||||||
$scope.Features = Features;
|
$scope.Features = Features;
|
||||||
$scope.Config = Config;
|
$scope.Config = Config;
|
||||||
$scope.KeyService = KeyService;
|
$scope.KeyService = KeyService;
|
||||||
|
|
||||||
|
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||||
|
$scope.externalLoginInfo = {};
|
||||||
|
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
|
||||||
|
|
||||||
UserService.updateUserIn($scope, function(user) {
|
UserService.updateUserIn($scope, function(user) {
|
||||||
$scope.cuser = jQuery.extend({}, user);
|
$scope.cuser = jQuery.extend({}, user);
|
||||||
|
$scope.externalLoginInfo = {};
|
||||||
|
|
||||||
if ($scope.cuser.logins) {
|
if ($scope.cuser.logins) {
|
||||||
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
||||||
var login = $scope.cuser.logins[i];
|
var login = $scope.cuser.logins[i];
|
||||||
login.metadata = login.metadata || {};
|
login.metadata = login.metadata || {};
|
||||||
|
$scope.externalLoginInfo[login.service] = login;
|
||||||
if (login.service == 'github') {
|
|
||||||
$scope.hasGithubLogin = true;
|
|
||||||
$scope.githubLogin = login.metadata['service_username'];
|
|
||||||
$scope.githubEndpoint = KeyService['githubEndpoint'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (login.service == 'google') {
|
|
||||||
$scope.hasGoogleLogin = true;
|
|
||||||
$scope.googleLogin = login.metadata['service_username'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.detachExternalLogin = function(kind) {
|
$scope.detachExternalLogin = function(kind) {
|
||||||
|
if (!Features.DIRECT_LOGIN) { return; }
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'servicename': kind
|
'servicename': kind
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.detachExternalLogin(null, params).then(function() {
|
ApiService.detachExternalLogin(null, params).then(function() {
|
||||||
$scope.hasGithubLogin = false;
|
|
||||||
$scope.hasGoogleLogin = false;
|
|
||||||
UserService.load();
|
UserService.load();
|
||||||
}, ApiService.errorDisplay('Count not detach service'));
|
}, ApiService.errorDisplay('Count not detach service'));
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,10 @@ angular.module('quay').directive('headerBar', function () {
|
||||||
},
|
},
|
||||||
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
|
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
|
||||||
PlanService, ApiService, NotificationService, Config, CreateService, Features,
|
PlanService, ApiService, NotificationService, Config, CreateService, Features,
|
||||||
DocumentationService) {
|
DocumentationService, ExternalLoginService) {
|
||||||
|
|
||||||
|
$scope.externalSigninUrl = ExternalLoginService.getSingleSigninUrl();
|
||||||
|
|
||||||
var hotkeysAdded = false;
|
var hotkeysAdded = false;
|
||||||
var userUpdated = function(cUser) {
|
var userUpdated = function(cUser) {
|
||||||
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
||||||
|
|
|
@ -14,10 +14,12 @@ angular.module('quay').directive('signinForm', function () {
|
||||||
'signInStarted': '&signInStarted',
|
'signInStarted': '&signInStarted',
|
||||||
'signedIn': '&signedIn'
|
'signedIn': '&signedIn'
|
||||||
},
|
},
|
||||||
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config, ExternalLoginService) {
|
||||||
$scope.tryAgainSoon = 0;
|
$scope.tryAgainSoon = 0;
|
||||||
$scope.tryAgainInterval = null;
|
$scope.tryAgainInterval = null;
|
||||||
$scope.signingIn = false;
|
$scope.signingIn = false;
|
||||||
|
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||||
|
$scope.Features = Features;
|
||||||
|
|
||||||
$scope.markStarted = function() {
|
$scope.markStarted = function() {
|
||||||
$scope.signingIn = true;
|
$scope.signingIn = true;
|
||||||
|
@ -45,7 +47,7 @@ angular.module('quay').directive('signinForm', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.signin = function() {
|
$scope.signin = function() {
|
||||||
if ($scope.tryAgainSoon > 0) { return; }
|
if ($scope.tryAgainSoon > 0 || !Features.DIRECT_LOGIN) { return; }
|
||||||
|
|
||||||
$scope.markStarted();
|
$scope.markStarted();
|
||||||
$scope.cancelInterval();
|
$scope.cancelInterval();
|
||||||
|
|
|
@ -13,11 +13,13 @@ angular.module('quay').directive('signupForm', function () {
|
||||||
'hideRegisteredMessage': '@hideRegisteredMessage',
|
'hideRegisteredMessage': '@hideRegisteredMessage',
|
||||||
'userRegistered': '&userRegistered'
|
'userRegistered': '&userRegistered'
|
||||||
},
|
},
|
||||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
|
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService, ExternalLoginService) {
|
||||||
$('.form-signup').popover();
|
$('.form-signup').popover();
|
||||||
|
|
||||||
$scope.awaitingConfirmation = false;
|
$scope.awaitingConfirmation = false;
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
|
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||||
|
$scope.singleSigninUrl = ExternalLoginService.getSingleSigninUrl();
|
||||||
|
|
||||||
$scope.register = function() {
|
$scope.register = function() {
|
||||||
UIService.hidePopover('#signupButton');
|
UIService.hidePopover('#signupButton');
|
||||||
|
|
|
@ -8,7 +8,12 @@
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
function SignInCtrl($scope, $location) {
|
function SignInCtrl($scope, $location, ExternalLoginService, Features) {
|
||||||
$scope.redirectUrl = '/';
|
$scope.redirectUrl = '/';
|
||||||
|
|
||||||
|
var singleUrl = ExternalLoginService.getSingleSigninUrl();
|
||||||
|
if (singleUrl) {
|
||||||
|
document.location = singleUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
})
|
})
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService, AvatarService, Config) {
|
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService, AvatarService, Config, ExternalLoginService) {
|
||||||
var username = $routeParams.username;
|
var username = $routeParams.username;
|
||||||
|
|
||||||
$scope.showInvoicesCounter = 0;
|
$scope.showInvoicesCounter = 0;
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
$scope.showRobotsCounter = 0;
|
$scope.showRobotsCounter = 0;
|
||||||
$scope.changeEmailInfo = {};
|
$scope.changeEmailInfo = {};
|
||||||
$scope.changePasswordInfo = {};
|
$scope.changePasswordInfo = {};
|
||||||
|
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
|
||||||
|
|
||||||
UserService.updateUserIn($scope);
|
UserService.updateUserIn($scope);
|
||||||
|
|
||||||
|
|
141
static/js/services/external-login-service.js
Normal file
141
static/js/services/external-login-service.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* Service which exposes the supported external logins.
|
||||||
|
*/
|
||||||
|
angular.module('quay').factory('ExternalLoginService', ['KeyService', 'Features', 'Config',
|
||||||
|
function(KeyService, Features, Config) {
|
||||||
|
var externalLoginService = {};
|
||||||
|
|
||||||
|
externalLoginService.getLoginUrl = function(service, action) {
|
||||||
|
var serviceInfo = externalLoginService.getProvider(service);
|
||||||
|
if (!serviceInfo) { return ''; }
|
||||||
|
|
||||||
|
var stateClause = '';
|
||||||
|
|
||||||
|
if (Config.MIXPANEL_KEY && window.mixpanel) {
|
||||||
|
if (mixpanel.get_distinct_id !== undefined) {
|
||||||
|
stateClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginUrl = KeyService.getConfiguration(serviceInfo.key, 'AUTHORIZE_ENDPOINT');
|
||||||
|
var clientId = KeyService.getConfiguration(serviceInfo.key, 'CLIENT_ID');
|
||||||
|
|
||||||
|
var scope = serviceInfo.scopes();
|
||||||
|
var redirectUri = Config.getUrl('/oauth2/' + service + '/callback');
|
||||||
|
|
||||||
|
if (action == 'attach') {
|
||||||
|
redirectUri += '/attach';
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = loginUrl + 'client_id=' + clientId + '&scope=' + scope + '&redirect_uri=' +
|
||||||
|
redirectUri + stateClause;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
var DEX = {
|
||||||
|
id: 'dex',
|
||||||
|
key: 'DEX_LOGIN_CONFIG',
|
||||||
|
|
||||||
|
title: function() {
|
||||||
|
return KeyService.getConfiguration('DEX_LOGIN_CONFIG', 'OIDC_TITLE');
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: function() {
|
||||||
|
return {'url': KeyService.getConfiguration('DEX_LOGIN_CONFIG', 'OIDC_LOGO') };
|
||||||
|
},
|
||||||
|
|
||||||
|
scopes: function() {
|
||||||
|
return 'openid email profile'
|
||||||
|
},
|
||||||
|
|
||||||
|
enabled: Features.DEX_LOGIN
|
||||||
|
};
|
||||||
|
|
||||||
|
var GITHUB = {
|
||||||
|
id: 'github',
|
||||||
|
key: 'GITHUB_LOGIN_CONFIG',
|
||||||
|
|
||||||
|
title: function() {
|
||||||
|
return KeyService.isEnterprise('github') ? 'GitHub Enterprise' : 'GitHub';
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: function() {
|
||||||
|
return {'icon': 'fa-github'};
|
||||||
|
},
|
||||||
|
|
||||||
|
hasUserInfo: true,
|
||||||
|
getUserInfo: function(service_info) {
|
||||||
|
username = service_info['metadata']['service_username'];
|
||||||
|
return {
|
||||||
|
'username': username,
|
||||||
|
'endpoint': KeyService['githubEndpoint'] + username
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scopes: function() {
|
||||||
|
var scopes = 'user:email';
|
||||||
|
if (KeyService.getConfiguration('GITHUB_LOGIN_CONFIG', 'ORG_RESTRICT')) {
|
||||||
|
scopes += ' read:org';
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes;
|
||||||
|
},
|
||||||
|
|
||||||
|
enabled: Features.GITHUB_LOGIN
|
||||||
|
};
|
||||||
|
|
||||||
|
var GOOGLE = {
|
||||||
|
id: 'google',
|
||||||
|
key: 'GOOGLE_LOGIN_CONFIG',
|
||||||
|
|
||||||
|
title: function() {
|
||||||
|
return 'Google';
|
||||||
|
},
|
||||||
|
|
||||||
|
icon: function() {
|
||||||
|
return {'icon': 'fa-google'};
|
||||||
|
},
|
||||||
|
|
||||||
|
scopes: function() {
|
||||||
|
return 'openid email';
|
||||||
|
},
|
||||||
|
|
||||||
|
enabled: Features.GOOGLE_LOGIN
|
||||||
|
};
|
||||||
|
|
||||||
|
externalLoginService.ALL_EXTERNAL_LOGINS = [
|
||||||
|
DEX, GITHUB, GOOGLE
|
||||||
|
];
|
||||||
|
|
||||||
|
externalLoginService.EXTERNAL_LOGINS = externalLoginService.ALL_EXTERNAL_LOGINS.filter(function(el) {
|
||||||
|
return el.enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
externalLoginService.getProvider = function(providerId) {
|
||||||
|
for (var i = 0; i < externalLoginService.EXTERNAL_LOGINS.length; ++i) {
|
||||||
|
var current = externalLoginService.EXTERNAL_LOGINS[i];
|
||||||
|
if (current.id == providerId) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
externalLoginService.hasSingleSignin = function() {
|
||||||
|
return externalLoginService.EXTERNAL_LOGINS.length == 1 && !Features.DIRECT_LOGIN;
|
||||||
|
};
|
||||||
|
|
||||||
|
externalLoginService.getSingleSigninUrl = function() {
|
||||||
|
// If there is a single external login service and direct login is disabled,
|
||||||
|
// then redirect to the external login directly.
|
||||||
|
if (externalLoginService.hasSingleSignin()) {
|
||||||
|
return externalLoginService.getLoginUrl(externalLoginService.EXTERNAL_LOGINS[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return externalLoginService;
|
||||||
|
}]);
|
|
@ -10,35 +10,26 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l
|
||||||
|
|
||||||
keyService['gitlabTriggerClientId'] = oauth['GITLAB_TRIGGER_CONFIG']['CLIENT_ID'];
|
keyService['gitlabTriggerClientId'] = oauth['GITLAB_TRIGGER_CONFIG']['CLIENT_ID'];
|
||||||
keyService['githubTriggerClientId'] = oauth['GITHUB_TRIGGER_CONFIG']['CLIENT_ID'];
|
keyService['githubTriggerClientId'] = oauth['GITHUB_TRIGGER_CONFIG']['CLIENT_ID'];
|
||||||
keyService['githubLoginClientId'] = oauth['GITHUB_LOGIN_CONFIG']['CLIENT_ID'];
|
|
||||||
keyService['googleLoginClientId'] = oauth['GOOGLE_LOGIN_CONFIG']['CLIENT_ID'];
|
|
||||||
|
|
||||||
keyService['gitlabRedirectUri'] = Config.getUrl('/oauth2/gitlab/callback');
|
keyService['gitlabRedirectUri'] = Config.getUrl('/oauth2/gitlab/callback');
|
||||||
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
|
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
|
||||||
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
|
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
|
||||||
|
|
||||||
keyService['githubLoginUrl'] = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
|
|
||||||
keyService['googleLoginUrl'] = oauth['GOOGLE_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
|
|
||||||
|
|
||||||
keyService['githubEndpoint'] = oauth['GITHUB_LOGIN_CONFIG']['GITHUB_ENDPOINT'];
|
|
||||||
|
|
||||||
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
|
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
|
||||||
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||||
|
|
||||||
keyService['gitlabTriggerEndpoint'] = oauth['GITLAB_TRIGGER_CONFIG']['GITLAB_ENDPOINT'];
|
keyService['gitlabTriggerEndpoint'] = oauth['GITLAB_TRIGGER_CONFIG']['GITLAB_ENDPOINT'];
|
||||||
keyService['gitlabTriggerAuthorizeUrl'] = oauth['GITLAB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
keyService['gitlabTriggerAuthorizeUrl'] = oauth['GITLAB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||||
|
|
||||||
keyService['githubLoginScope'] = 'user:email';
|
keyService.getConfiguration = function(parent, key) {
|
||||||
if (oauth['GITHUB_LOGIN_CONFIG']['ORG_RESTRICT']) {
|
return oauth[parent][key];
|
||||||
keyService['githubLoginScope'] += ',read:org';
|
};
|
||||||
}
|
|
||||||
|
|
||||||
keyService['googleLoginScope'] = 'openid email';
|
|
||||||
|
|
||||||
keyService.isEnterprise = function(service) {
|
keyService.isEnterprise = function(service) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case 'github':
|
case 'github':
|
||||||
return keyService['githubLoginUrl'].indexOf('https://github.com/') < 0;
|
var loginUrl = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||||
|
return loginUrl.indexOf('https://github.com/') < 0;
|
||||||
|
|
||||||
case 'github-trigger':
|
case 'github-trigger':
|
||||||
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
|
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
|
||||||
|
@ -47,26 +38,5 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
keyService.getExternalLoginUrl = function(service, action) {
|
|
||||||
var state_clause = '';
|
|
||||||
if (Config.MIXPANEL_KEY && window.mixpanel) {
|
|
||||||
if (mixpanel.get_distinct_id !== undefined) {
|
|
||||||
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var client_id = keyService[service + 'LoginClientId'];
|
|
||||||
var scope = keyService[service + 'LoginScope'];
|
|
||||||
var redirect_uri = keyService[service + 'RedirectUri'];
|
|
||||||
if (action == 'attach') {
|
|
||||||
redirect_uri += '/attach';
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
|
|
||||||
'&redirect_uri=' + redirect_uri + state_clause;
|
|
||||||
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
return keyService;
|
return keyService;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
tab-init="showInvoices()" quay-show="Features.BILLING">
|
tab-init="showInvoices()" quay-show="Features.BILLING">
|
||||||
<i class="fa ci-invoice"></i>
|
<i class="fa ci-invoice"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="cor-tab" tab-title="External Logins" tab-target="#external">
|
<span class="cor-tab" tab-title="External Logins" tab-target="#external"
|
||||||
|
quay-show="!hasSingleSignin">
|
||||||
<i class="fa fa-external-link-square"></i>
|
<i class="fa fa-external-link-square"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="cor-tab" tab-title="Authorized Applications" tab-target="#applications"
|
<span class="cor-tab" tab-title="Authorized Applications" tab-target="#applications"
|
||||||
|
@ -70,7 +71,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- External Logins -->
|
<!-- External Logins -->
|
||||||
<div id="external" class="tab-pane">
|
<div id="external" class="tab-pane" quay-show="!hasSingleSignin">
|
||||||
<div class="external-logins-manager" user="viewuser"></div>
|
<div class="external-logins-manager" user="viewuser"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<h2 style="margin-bottom: 20px;">There was an error logging in with {{ service_name }}.</h2>
|
<h2 style="margin-bottom: 20px;">There was an error logging in with {{ service_name }}.</h2>
|
||||||
|
|
||||||
{% if error_message %}
|
{% if error_message %}
|
||||||
<div class="alert alert-danger">{{ error_message }}</div>
|
<div class="co-alert co-alert-danger">{{ error_message }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user_creation %}
|
{% if user_creation %}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import urlparse
|
import urlparse
|
||||||
import github
|
import github
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from cachetools.func import TTLCache
|
||||||
|
from jwkest.jwk import KEYS, keyrep
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class OAuthConfig(object):
|
class OAuthConfig(object):
|
||||||
def __init__(self, config, key_name):
|
def __init__(self, config, key_name):
|
||||||
|
@ -38,10 +46,8 @@ class OAuthConfig(object):
|
||||||
|
|
||||||
|
|
||||||
def exchange_code_for_token(self, app_config, http_client, code, form_encode=False,
|
def exchange_code_for_token(self, app_config, http_client, code, form_encode=False,
|
||||||
redirect_suffix=''):
|
redirect_suffix='', client_auth=False):
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': self.client_id(),
|
|
||||||
'client_secret': self.client_secret(),
|
|
||||||
'code': code,
|
'code': code,
|
||||||
'grant_type': 'authorization_code',
|
'grant_type': 'authorization_code',
|
||||||
'redirect_uri': self.get_redirect_uri(app_config, redirect_suffix)
|
'redirect_uri': self.get_redirect_uri(app_config, redirect_suffix)
|
||||||
|
@ -51,11 +57,18 @@ class OAuthConfig(object):
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if client_auth:
|
||||||
|
auth = (self.client_id(), self.client_secret())
|
||||||
|
else:
|
||||||
|
payload['client_id'] = self.client_id()
|
||||||
|
payload['client_secret'] = self.client_secret()
|
||||||
|
|
||||||
token_url = self.token_endpoint()
|
token_url = self.token_endpoint()
|
||||||
if form_encode:
|
if form_encode:
|
||||||
get_access_token = http_client.post(token_url, data=payload, headers=headers)
|
get_access_token = http_client.post(token_url, data=payload, headers=headers, auth=auth)
|
||||||
else:
|
else:
|
||||||
get_access_token = http_client.post(token_url, params=payload, headers=headers)
|
get_access_token = http_client.post(token_url, params=payload, headers=headers, auth=auth)
|
||||||
|
|
||||||
json_data = get_access_token.json()
|
json_data = get_access_token.json()
|
||||||
if not json_data:
|
if not json_data:
|
||||||
|
@ -248,3 +261,102 @@ class GitLabOAuthConfig(OAuthConfig):
|
||||||
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
|
||||||
'GITLAB_ENDPOINT': self._endpoint(),
|
'GITLAB_ENDPOINT': self._endpoint(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OIDC_WELLKNOWN = ".well-known/openid-configuration"
|
||||||
|
PUBLIC_KEY_CACHE_TTL = 3600 # 1 hour
|
||||||
|
|
||||||
|
class OIDCConfig(OAuthConfig):
|
||||||
|
def __init__(self, config, key_name):
|
||||||
|
super(OIDCConfig, self).__init__(config, key_name)
|
||||||
|
|
||||||
|
self._public_key_cache = TTLCache(1, PUBLIC_KEY_CACHE_TTL, missing=self._get_public_key)
|
||||||
|
self._oidc_config = {}
|
||||||
|
self._http_client = config['HTTPCLIENT']
|
||||||
|
|
||||||
|
if self.config.get('OIDC_SERVER'):
|
||||||
|
self._load_via_discovery(config['DEBUGGING'])
|
||||||
|
|
||||||
|
def _load_via_discovery(self, is_debugging):
|
||||||
|
oidc_server = self.config['OIDC_SERVER']
|
||||||
|
if not oidc_server.startswith('https://') and not is_debugging:
|
||||||
|
raise Exception('OIDC server must be accessed over SSL')
|
||||||
|
|
||||||
|
discovery_url = urlparse.urljoin(oidc_server, OIDC_WELLKNOWN)
|
||||||
|
discovery = self._http_client.get(discovery_url, timeout=5)
|
||||||
|
|
||||||
|
if discovery.status_code / 100 != 2:
|
||||||
|
raise Exception("Could not load OIDC discovery information")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._oidc_config = json.loads(discovery.text)
|
||||||
|
except ValueError:
|
||||||
|
logger.exception('Could not parse OIDC discovery for url: %s', discovery_url)
|
||||||
|
raise Exception("Could not parse OIDC discovery information")
|
||||||
|
|
||||||
|
def authorize_endpoint(self):
|
||||||
|
return self._oidc_config['authorization_endpoint'] + '?'
|
||||||
|
|
||||||
|
def token_endpoint(self):
|
||||||
|
return self._oidc_config['token_endpoint']
|
||||||
|
|
||||||
|
def user_endpoint(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_client_id_and_secret(self, http_client, app_config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_public_config(self):
|
||||||
|
return {
|
||||||
|
'CLIENT_ID': self.client_id(),
|
||||||
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issuer(self):
|
||||||
|
return self.config.get('OIDC_ISSUER', self.config['OIDC_SERVER'])
|
||||||
|
|
||||||
|
def get_public_key(self, force_refresh=False):
|
||||||
|
""" Retrieves the public key for this handler. """
|
||||||
|
# If force_refresh is true, we expire all the items in the cache by setting the time to
|
||||||
|
# the current time + the expiration TTL.
|
||||||
|
if force_refresh:
|
||||||
|
self._public_key_cache.expire(time=time.time() + PUBLIC_KEY_CACHE_TTL)
|
||||||
|
|
||||||
|
# Retrieve the public key from the cache. If the cache does not contain the public key,
|
||||||
|
# it will internally call _get_public_key to retrieve it and then save it. The None is
|
||||||
|
# a random key chose to be stored in the cache, and could be anything.
|
||||||
|
return self._public_key_cache[None]
|
||||||
|
|
||||||
|
def _get_public_key(self):
|
||||||
|
""" Retrieves the public key for this handler. """
|
||||||
|
keys_url = self._oidc_config['jwks_uri']
|
||||||
|
|
||||||
|
keys = KEYS()
|
||||||
|
keys.load_from_url(keys_url)
|
||||||
|
|
||||||
|
if not list(keys):
|
||||||
|
raise Exception('No keys provided by OIDC provider')
|
||||||
|
|
||||||
|
rsa_key = list(keys)[0]
|
||||||
|
rsa_key.deserialize()
|
||||||
|
return rsa_key.key.exportKey('PEM')
|
||||||
|
|
||||||
|
|
||||||
|
class DexOAuthConfig(OIDCConfig):
|
||||||
|
def service_name(self):
|
||||||
|
return 'Dex'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_title(self):
|
||||||
|
return self.get_public_config()['OIDC_TITLE']
|
||||||
|
|
||||||
|
def get_public_config(self):
|
||||||
|
return {
|
||||||
|
'CLIENT_ID': self.client_id(),
|
||||||
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
|
||||||
|
|
||||||
|
# TODO(jschorr): This should ideally come from the Dex side.
|
||||||
|
'OIDC_TITLE': 'Dex',
|
||||||
|
'OIDC_LOGO': 'https://tectonic.com/assets/ico/favicon-96x96.png'
|
||||||
|
}
|
||||||
|
|
Reference in a new issue