1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 15:55:23 +02:00

feat(auth): integrate oauth extension (#4152)

* refactor(oauth): move oauth client code

* feat(oauth): move extension code into server code

* feat(oauth): enable oauth without extension

* refactor(oauth): make it easier to remove providers
This commit is contained in:
Chaim Lev-Ari 2020-08-05 11:36:46 +03:00 committed by GitHub
parent 148ccd1bc4
commit 00f4fe0039
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 201 additions and 135 deletions

View file

@ -25,7 +25,7 @@ function initAnalytics(Analytics, $rootScope) {
});
}
angular.module('portainer.app', []).config([
angular.module('portainer.app', ['portainer.oauth']).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';

View file

@ -0,0 +1 @@
angular.module('portainer.oauth', ['ngResource']).constant('API_ENDPOINT_OAUTH', 'api/auth/oauth');

View file

@ -0,0 +1,72 @@
angular.module('portainer.oauth').controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
var ctrl = this;
this.providers = [
{
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
userIdentifier: 'userPrincipalName',
scopes: 'id,email,name',
name: 'microsoft',
label: 'Microsoft',
description: 'Microsoft OAuth provider',
icon: 'fab fa-microsoft',
},
{
authUrl: 'https://accounts.google.com/o/oauth2/auth',
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
userIdentifier: 'email',
scopes: 'profile email',
name: 'google',
label: 'Google',
description: 'Google OAuth provider',
icon: 'fab fa-google',
},
{
authUrl: 'https://github.com/login/oauth/authorize',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
resourceUrl: 'https://api.github.com/user',
userIdentifier: 'login',
scopes: 'id email name',
name: 'github',
label: 'Github',
description: 'Github OAuth provider',
icon: 'fab fa-github',
},
{
authUrl: '',
accessTokenUrl: '',
resourceUrl: '',
userIdentifier: '',
scopes: '',
name: 'custom',
label: 'Custom',
description: 'Custom OAuth provider',
icon: 'fa fa-user-check',
},
];
this.$onInit = onInit;
function onInit() {
if (ctrl.provider.authUrl) {
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
} else {
ctrl.provider = ctrl.providers[0];
}
ctrl.onSelect(ctrl.provider, false);
}
function getProviderByURL(providerAuthURL) {
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
return ctrl.providers[0];
} else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
return ctrl.providers[1];
} else if (providerAuthURL.indexOf('github.com') !== -1) {
return ctrl.providers[2];
}
return ctrl.providers[3];
}
});

View file

@ -0,0 +1,19 @@
<div class="col-sm-12 form-section-title">
Provider
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div ng-repeat="provider in $ctrl.providers" ng-click="$ctrl.onSelect(provider, true)">
<input type="radio" id="{{ 'oauth_provider_' + provider.name }}" ng-model="$ctrl.provider" ng-value="provider" />
<label for="{{ 'oauth_provider_' + provider.name }}">
<div class="boxselector_header">
<i ng-class="provider.icon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ provider.label }}
</div>
<p>{{ provider.description }}</p>
</label>
</div>
</div>
</div>

View file

@ -0,0 +1,8 @@
angular.module('portainer.oauth').component('oauthProvidersSelector', {
templateUrl: './oauth-providers-selector.html',
bindings: {
onSelect: '<',
provider: '=',
},
controller: 'OAuthProviderSelectorController',
});

View file

@ -0,0 +1,75 @@
import _ from 'lodash-es';
angular.module('portainer.oauth').controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
this.state = {
provider: {},
overrideConfiguration: false,
microsoftTenantID: '',
};
this.$onInit = onInit;
this.onSelectProvider = onSelectProvider;
this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange;
this.useDefaultProviderConfiguration = useDefaultProviderConfiguration;
function onMicrosoftTenantIDChange() {
var tenantID = ctrl.state.microsoftTenantID;
ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID);
ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID);
ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID);
}
function useDefaultProviderConfiguration() {
ctrl.settings.AuthorizationURI = ctrl.state.provider.authUrl;
ctrl.settings.AccessTokenURI = ctrl.state.provider.accessTokenUrl;
ctrl.settings.ResourceURI = ctrl.state.provider.resourceUrl;
ctrl.settings.UserIdentifier = ctrl.state.provider.userIdentifier;
ctrl.settings.Scopes = ctrl.state.provider.scopes;
if (ctrl.state.provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function useExistingConfiguration() {
var provider = ctrl.state.provider;
ctrl.settings.AuthorizationURI = ctrl.settings.AuthorizationURI === '' ? provider.authUrl : ctrl.settings.AuthorizationURI;
ctrl.settings.AccessTokenURI = ctrl.settings.AccessTokenURI === '' ? provider.accessTokenUrl : ctrl.settings.AccessTokenURI;
ctrl.settings.ResourceURI = ctrl.settings.ResourceURI === '' ? provider.resourceUrl : ctrl.settings.ResourceURI;
ctrl.settings.UserIdentifier = ctrl.settings.UserIdentifier === '' ? provider.userIdentifier : ctrl.settings.UserIdentifier;
ctrl.settings.Scopes = ctrl.settings.Scopes === '' ? provider.scopes : ctrl.settings.Scopes;
if (provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function onSelectProvider(provider, overrideConfiguration) {
ctrl.state.provider = provider;
if (overrideConfiguration) {
useDefaultProviderConfiguration();
} else {
useExistingConfiguration();
}
}
function onInit() {
if (ctrl.settings.RedirectURI === '') {
ctrl.settings.RedirectURI = window.location.origin;
}
if (ctrl.settings.AuthorizationURI !== '') {
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
if (ctrl.settings.AuthorizationURI.indexOf('login.microsoftonline.com') > -1) {
var tenantID = ctrl.settings.AuthorizationURI.match(/login.microsoftonline.com\/(.*?)\//)[1];
ctrl.state.microsoftTenantID = tenantID;
onMicrosoftTenantIDChange();
}
}
}
});

View file

@ -0,0 +1,164 @@
<div>
<div class="col-sm-12 form-section-title">
Automatic user provisioning
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
With automatic user provisioning enabled, Portainer will create user(s) automatically with the standard user role. If disabled, users must be created beforehand in Portainer
in order to login.
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i> </label>
</div>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>The users created by the automatic provisioning feature can be added to a default team on creation.</p>
<p
>By assigning newly created users to a team, they will be able to access the environments associated to that team. This setting is optional and if not set, newly created
users won't be able to access any environments.</p
>
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Default team</label>
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
</span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"
><i class="fa fa-times" aria-hidden="true"></i
></button>
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
<option value="">No team</option>
</select>
</div>
</div>
</div>
<oauth-providers-selector on-select="($ctrl.onSelectProvider)" provider="$ctrl.state.provider"></oauth-providers-selector>
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'microsoft'">
<label for="oauth_microsoft_tenant_id" class="col-sm-3 col-lg-2 control-label text-left">
Tenant ID
<portainer-tooltip position="bottom" message="ID of the Azure Directory you wish to authenticate against. Also known as the Directory ID"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_microsoft_tenant_id"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-model="$ctrl.state.microsoftTenantID"
ng-change="$ctrl.onMicrosoftTenantIDChange()"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application ID' : 'Client ID' }}
<portainer-tooltip position="bottom" message="Public identifier of the OAuth application"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_client_id" ng-model="$ctrl.settings.ClientID" placeholder="xxxxxxxxxxxxxxxxxxxx" />
</div>
</div>
<div class="form-group">
<label for="oauth_client_secret" class="col-sm-3 col-lg-2 control-label text-left">
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application key' : 'Client secret' }}
</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="oauth_client_secret" ng-model="$ctrl.settings.ClientSecret" placeholder="xxxxxxxxxxxxxxxxxxxx" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URL
<portainer-tooltip
position="bottom"
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_authorization_uri" ng-model="$ctrl.settings.AuthorizationURI" placeholder="https://example.com/oauth/authorize" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access token URL
<portainer-tooltip position="bottom" message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_access_token_uri" ng-model="$ctrl.settings.AccessTokenURI" placeholder="https://example.com/oauth/token" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URL
<portainer-tooltip position="bottom" message="URL used by Portainer to retrieve information about the authenticated user"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_resource_uri" ng-model="$ctrl.settings.ResourceURI" placeholder="https://example.com/user" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URL
<portainer-tooltip
position="bottom"
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_redirect_uri" ng-model="$ctrl.settings.RedirectURI" placeholder="http://yourportainer.com/" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User identifier
<portainer-tooltip
position="bottom"
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_user_identifier" ng-model="$ctrl.settings.UserIdentifier" placeholder="id" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes
<portainer-tooltip
position="bottom"
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_scopes" ng-model="$ctrl.settings.Scopes" placeholder="id,email,name" />
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name != 'custom'">
<div class="col-sm-12">
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Override default configuration
</a>
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.useDefaultProviderConfiguration()">
<i class="fa fa-cogs space-right" aria-hidden="true"></i> Use default configuration
</a>
</div>
</div>
</div>

View file

@ -0,0 +1,8 @@
angular.module('portainer.oauth').component('oauthSettings', {
templateUrl: './oauth-settings.html',
bindings: {
settings: '=',
teams: '<',
},
controller: 'OAuthSettingsController',
});

View file

@ -0,0 +1,20 @@
angular.module('portainer.oauth').factory('OAuth', [
'$resource',
'API_ENDPOINT_OAUTH',
function OAuthFactory($resource, API_ENDPOINT_OAUTH) {
'use strict';
return $resource(
API_ENDPOINT_OAUTH + '/:action',
{},
{
validate: {
method: 'POST',
ignoreLoadingBar: true,
params: {
action: 'validate',
},
},
}
);
},
]);

View file

@ -57,7 +57,7 @@
<p>LDAP authentication</p>
</label>
</div>
<div ng-if="oauthAuthenticationAvailable">
<div>
<input type="radio" id="registry_auth" ng-model="settings.AuthenticationMethod" ng-value="3" />
<label for="registry_auth">
<div class="boxselector_header">
@ -67,23 +67,6 @@
<p>OAuth authentication</p>
</label>
</div>
<div style="color: #767676;" ng-click="goToOAuthExtensionView()" ng-if="!oauthAuthenticationAvailable">
<input type="radio" id="registry_auth" ng-model="settings.AuthenticationMethod" ng-value="3" disabled />
<label
for="registry_auth"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Feature available via an extension"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
OAuth (extension)
</div>
<p>OAuth authentication</p>
</label>
</div>
</div>
</div>

View file

@ -6,8 +6,7 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
'SettingsService',
'FileUploadService',
'TeamService',
'ExtensionService',
function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, ExtensionService) {
function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService) {
$scope.state = {
successfulConnectivityCheck: false,
failedConnectivityCheck: false,
@ -68,10 +67,6 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
},
};
$scope.goToOAuthExtensionView = function () {
$state.go('portainer.extensions.extension', { id: 2 });
};
$scope.isOauthEnabled = function isOauthEnabled() {
return $scope.settings && $scope.settings.AuthenticationMethod === 3;
};
@ -167,7 +162,6 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
$q.all({
settings: SettingsService.settings(),
teams: TeamService.teams(),
oauthAuthentication: ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.OAUTH_AUTHENTICATION),
})
.then(function success(data) {
var settings = data.settings;
@ -176,7 +170,6 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [
$scope.formValues.LDAPSettings = settings.LDAPSettings;
$scope.OAuthSettings = settings.OAuthSettings;
$scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert;
$scope.oauthAuthenticationAvailable = data.oauthAuthentication;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');