Add support for Dex to Quay
Fixes #306 - Adds support for Dex as an OAuth external login provider - Adds support for OIDC in general - Extract out external logins on the JS side into a service - Add a feature flag for disabling direct login - Add support for directing to the single external login service - Does *not* yet support the config in the superuser tool
This commit is contained in:
parent
46f150cafb
commit
c0286d1ac3
27 changed files with 533 additions and 176 deletions
|
@ -15,14 +15,14 @@ angular.module('quay').directive('externalLoginButton', function () {
|
|||
'provider': '@provider',
|
||||
'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.isEnterprise = KeyService.isEnterprise;
|
||||
$scope.providerInfo = ExternalLoginService.getProvider($scope.provider);
|
||||
|
||||
$scope.startSignin = function(service) {
|
||||
$scope.signInStarted({'service': service});
|
||||
$scope.startSignin = function() {
|
||||
$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.
|
||||
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||
|
|
|
@ -11,41 +11,37 @@ angular.module('quay').directive('externalLoginsManager', function () {
|
|||
scope: {
|
||||
'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.Config = Config;
|
||||
$scope.KeyService = KeyService;
|
||||
|
||||
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||
$scope.externalLoginInfo = {};
|
||||
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
|
||||
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
$scope.cuser = jQuery.extend({}, user);
|
||||
$scope.externalLoginInfo = {};
|
||||
|
||||
if ($scope.cuser.logins) {
|
||||
for (var i = 0; i < $scope.cuser.logins.length; i++) {
|
||||
var login = $scope.cuser.logins[i];
|
||||
login.metadata = login.metadata || {};
|
||||
|
||||
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.externalLoginInfo[login.service] = login;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.detachExternalLogin = function(kind) {
|
||||
if (!Features.DIRECT_LOGIN) { return; }
|
||||
|
||||
var params = {
|
||||
'servicename': kind
|
||||
};
|
||||
|
||||
ApiService.detachExternalLogin(null, params).then(function() {
|
||||
$scope.hasGithubLogin = false;
|
||||
$scope.hasGoogleLogin = false;
|
||||
UserService.load();
|
||||
}, 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,
|
||||
PlanService, ApiService, NotificationService, Config, CreateService, Features,
|
||||
DocumentationService) {
|
||||
DocumentationService, ExternalLoginService) {
|
||||
|
||||
$scope.externalSigninUrl = ExternalLoginService.getSingleSigninUrl();
|
||||
|
||||
var hotkeysAdded = false;
|
||||
var userUpdated = function(cUser) {
|
||||
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
||||
|
|
|
@ -14,10 +14,12 @@ angular.module('quay').directive('signinForm', function () {
|
|||
'signInStarted': '&signInStarted',
|
||||
'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.tryAgainInterval = null;
|
||||
$scope.signingIn = false;
|
||||
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||
$scope.Features = Features;
|
||||
|
||||
$scope.markStarted = function() {
|
||||
$scope.signingIn = true;
|
||||
|
@ -45,7 +47,7 @@ angular.module('quay').directive('signinForm', function () {
|
|||
});
|
||||
|
||||
$scope.signin = function() {
|
||||
if ($scope.tryAgainSoon > 0) { return; }
|
||||
if ($scope.tryAgainSoon > 0 || !Features.DIRECT_LOGIN) { return; }
|
||||
|
||||
$scope.markStarted();
|
||||
$scope.cancelInterval();
|
||||
|
|
|
@ -13,11 +13,13 @@ angular.module('quay').directive('signupForm', function () {
|
|||
'hideRegisteredMessage': '@hideRegisteredMessage',
|
||||
'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();
|
||||
|
||||
$scope.awaitingConfirmation = false;
|
||||
$scope.registering = false;
|
||||
$scope.EXTERNAL_LOGINS = ExternalLoginService.EXTERNAL_LOGINS;
|
||||
$scope.singleSigninUrl = ExternalLoginService.getSingleSigninUrl();
|
||||
|
||||
$scope.register = function() {
|
||||
UIService.hidePopover('#signupButton');
|
||||
|
|
|
@ -8,7 +8,12 @@
|
|||
});
|
||||
}]);
|
||||
|
||||
function SignInCtrl($scope, $location) {
|
||||
function SignInCtrl($scope, $location, ExternalLoginService, Features) {
|
||||
$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;
|
||||
|
||||
$scope.showInvoicesCounter = 0;
|
||||
|
@ -18,6 +18,7 @@
|
|||
$scope.showRobotsCounter = 0;
|
||||
$scope.changeEmailInfo = {};
|
||||
$scope.changePasswordInfo = {};
|
||||
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
|
||||
|
||||
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['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['githubRedirectUri'] = Config.getUrl('/oauth2/github/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['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||
|
||||
keyService['gitlabTriggerEndpoint'] = oauth['GITLAB_TRIGGER_CONFIG']['GITLAB_ENDPOINT'];
|
||||
keyService['gitlabTriggerAuthorizeUrl'] = oauth['GITLAB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||
|
||||
keyService['githubLoginScope'] = 'user:email';
|
||||
if (oauth['GITHUB_LOGIN_CONFIG']['ORG_RESTRICT']) {
|
||||
keyService['githubLoginScope'] += ',read:org';
|
||||
}
|
||||
|
||||
keyService['googleLoginScope'] = 'openid email';
|
||||
keyService.getConfiguration = function(parent, key) {
|
||||
return oauth[parent][key];
|
||||
};
|
||||
|
||||
keyService.isEnterprise = function(service) {
|
||||
switch (service) {
|
||||
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':
|
||||
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
|
||||
|
@ -47,26 +38,5 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l
|
|||
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;
|
||||
}]);
|
||||
|
|
Reference in a new issue