Add an AppSpecificAuthToken data model for app-specific auth tokens. These will be used for the Docker CLI in place of username+password
This commit is contained in:
parent
53b762a875
commit
524d77f527
50 changed files with 943 additions and 289 deletions
|
@ -622,14 +622,14 @@
|
|||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
<p>
|
||||
Authentication for the registry can be handled by either the registry itself, LDAP, Keystone, OIDC or external JWT endpoint.
|
||||
Authentication for the registry can be handled by either the registry itself, LDAP, Keystone, or external JWT endpoint.
|
||||
</p>
|
||||
<p>
|
||||
Additional <strong>external</strong> authentication providers (such as GitHub) can be used in addition for <strong>login into the UI</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-if="config.AUTHENTICATION_TYPE != 'OIDC'">
|
||||
<div ng-if="config.AUTHENTICATION_TYPE != 'AppToken'">
|
||||
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE != 'Database' && !config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
It is <strong>highly recommended</strong> to require encrypted client passwords. External passwords used in the Docker client will be stored in <strong>plaintext</strong>!
|
||||
<a ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>.
|
||||
|
@ -650,7 +650,7 @@
|
|||
<option value="LDAP">LDAP</option>
|
||||
<option value="Keystone">Keystone (OpenStack Identity)</option>
|
||||
<option value="JWT">JWT Custom Authentication</option>
|
||||
<option value="OIDC">OIDC Token Authentication</option>
|
||||
<option value="AppToken" ng-if="config.FEATURE_APP_SPECIFIC_TOKENS">External Application Token</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -690,21 +690,6 @@
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- OIDC Token Authentication -->
|
||||
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'OIDC'">
|
||||
<tr>
|
||||
<td>OIDC Provider:</td>
|
||||
<td>
|
||||
<select class="form-control" ng-model="config.INTERNAL_OIDC_SERVICE_ID" ng-if="getOIDCProviders(config).length">
|
||||
<option value="{{ getOIDCProviderId(provider) }}" ng-repeat="provider in getOIDCProviders(config)">{{ config[provider]['SERVICE_NAME'] || getOIDCProviderId(provider) }}</option>
|
||||
</select>
|
||||
<div class="co-alert co-alert-danger" ng-if="!getOIDCProviders(config).length">
|
||||
An OIDC provider must be configured to use this authentication system
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Keystone Authentication -->
|
||||
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'">
|
||||
<tr>
|
||||
|
@ -782,7 +767,7 @@
|
|||
<div class="help-text">
|
||||
A certificate containing the public key portion of the key pair used to sign
|
||||
the JSON Web Tokens. This file must be in PEM format.
|
||||
</div
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -1091,7 +1076,7 @@
|
|||
<span style="display: inline-block; margin-left: 10px">(<a href="javascript:void(0)" ng-click="removeOIDCProvider(provider)">Delete</a>)</span>
|
||||
</div>
|
||||
<div class="co-panel-body">
|
||||
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE && config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'OIDC' && !(config[provider].LOGIN_BINDING_FIELD)">
|
||||
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE && config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'AppToken' && !(config[provider].LOGIN_BINDING_FIELD)">
|
||||
Warning: This OIDC provider is not bound to your <strong>{{ config.AUTHENTICATION_TYPE }}</strong> authentication. Logging in via this provider will create a <strong><span class="registry-name"></span>-only user</strong>, which is not the recommended approach. It is <strong>highly</strong> recommended to choose a "Binding Field" below.
|
||||
</div>
|
||||
|
||||
|
@ -1152,7 +1137,7 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'OIDC'">
|
||||
<tr ng-if="config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'AppToken'">
|
||||
<td>Binding Field:</td>
|
||||
<td>
|
||||
<select class="form-control" ng-model="config[provider].LOGIN_BINDING_FIELD">
|
||||
|
@ -1234,6 +1219,28 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="non-input">External Application tokens</td>
|
||||
<td colspan="2">
|
||||
<div class="config-bool-field" binding="config.FEATURE_APP_SPECIFIC_TOKENS">
|
||||
Allow external application tokens
|
||||
</div>
|
||||
<div class="help-text">
|
||||
If enabled, users will be able to generate external application tokens for use on the Docker and rkt CLI. Note
|
||||
that these tokens will <strong>not be required</strong> unless "App Token" is chosen as the Internal Authentication method above.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="config.FEATURE_APP_SPECIFIC_TOKENS">
|
||||
<td>External application token expiration</td>
|
||||
<td colspan="2">
|
||||
<span class="config-string-field" binding="config.APP_SPECIFIC_TOKEN_EXPIRATION"
|
||||
pattern="[0-9]+(m|w|h|d|s)" is-optional="true"></span>
|
||||
<div class="help-text">
|
||||
The expiration time for user generated external application tokens. If none, tokens will never expire.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="non-input">Anonymous Access:</td>
|
||||
<td colspan="2">
|
||||
|
|
|
@ -45,8 +45,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
return config.AUTHENTICATION_TYPE == 'Keystone';
|
||||
}, 'password': true},
|
||||
|
||||
{'id': 'oidc-auth', 'title': 'OIDC Authentication', 'condition': function(config) {
|
||||
return config.AUTHENTICATION_TYPE == 'OIDC';
|
||||
{'id': 'apptoken-auth', 'title': 'App Token Authentication', 'condition': function(config) {
|
||||
return config.AUTHENTICATION_TYPE == 'AppToken';
|
||||
}},
|
||||
|
||||
{'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<div class="resource-view" resource="$ctrl.appTokensResource">
|
||||
<div style="float: right; margin-left: 10px;">
|
||||
<button class="btn btn-primary" ng-click="$ctrl.askCreateToken()">Create Application Token</button>
|
||||
</div>
|
||||
<cor-table table-data="$ctrl.appTokens" table-item-title="tokens" filter-fields="['title']">
|
||||
<cor-table-col datafield="title" sortfield="title" title="Title" selected="true"
|
||||
bind-model="$ctrl"
|
||||
templateurl="/static/js/directives/ui/app-specific-token-manager/token-title.html"></cor-table-col>
|
||||
<cor-table-col datafield="last_accessed" sortfield="last_accessed" title="Last Accessed"
|
||||
kindof="datetime" templateurl="/static/js/directives/ui/app-specific-token-manager/last-accessed.html"></cor-table-col>
|
||||
<cor-table-col datafield="expiration" sortfield="expiration" title="Expiration"
|
||||
kindof="datetime" templateurl="/static/js/directives/ui/app-specific-token-manager/expiration.html"></cor-table-col>
|
||||
<cor-table-col datafield="created" sortfield="created" title="Created"
|
||||
kindof="datetime" templateurl="/static/js/directives/ui/app-specific-token-manager/created.html"></cor-table-col>
|
||||
<cor-table-col templateurl="/static/js/directives/ui/app-specific-token-manager/cog.html"
|
||||
bind-model="$ctrl" class="options-col"></cor-table-col>
|
||||
</cor-table>
|
||||
|
||||
<div class="credentials-dialog" credentials="$ctrl.tokenCredentials" secret-title="Application Token" entity-title="application token" entity-icon="fa-key"></div>
|
||||
|
||||
<!-- Revoke token confirm -->
|
||||
<div class="cor-confirm-dialog"
|
||||
dialog-context="$ctrl.revokeTokenInfo"
|
||||
dialog-action="$ctrl.revokeToken(info.token, callback)"
|
||||
dialog-title="Revoke Application Token"
|
||||
dialog-action-title="Revoke Token">
|
||||
<div class="co-alert co-alert-warning" style="margin-bottom: 10px;">
|
||||
Application token "{{ $ctrl.revokeTokenInfo.token.title }}" will be revoked and <strong>all</strong> applications and CLIs making use of the token will no longer operate.
|
||||
</div>
|
||||
|
||||
Proceed with revocation of this token?
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,71 @@
|
|||
import { Input, Component, Inject } from 'ng-metadata/core';
|
||||
import * as bootbox from "bootbox";
|
||||
|
||||
/**
|
||||
* A component that displays and manage all app specific tokens for a user.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-specific-token-manager',
|
||||
templateUrl: '/static/js/directives/ui/app-specific-token-manager/app-specific-token-manager.component.html',
|
||||
})
|
||||
export class AppSpecificTokenManagerComponent {
|
||||
private appTokensResource: any;
|
||||
private appTokens: Array<any>;
|
||||
private tokenCredentials: any;
|
||||
private revokeTokenInfo: any;
|
||||
|
||||
constructor(@Inject('ApiService') private ApiService: any, @Inject('UserService') private UserService: any) {
|
||||
this.loadTokens();
|
||||
}
|
||||
|
||||
private loadTokens() {
|
||||
this.appTokensResource = this.ApiService.listAppTokensAsResource().get((resp) => {
|
||||
this.appTokens = resp['tokens'];
|
||||
});
|
||||
}
|
||||
|
||||
private askCreateToken() {
|
||||
bootbox.prompt('Please enter a descriptive title for the new application token', (title) => {
|
||||
if (!title) { return; }
|
||||
|
||||
const errorHandler = this.ApiService.errorDisplay('Could not create the application token');
|
||||
this.ApiService.createAppToken({title}).then((resp) => {
|
||||
this.loadTokens();
|
||||
}, errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
private showRevokeToken(token) {
|
||||
this.revokeTokenInfo = {
|
||||
'token': token,
|
||||
};
|
||||
};
|
||||
|
||||
private revokeToken(token, callback) {
|
||||
const errorHandler = this.ApiService.errorDisplay('Could not revoke application token', callback);
|
||||
const params = {
|
||||
'token_uuid': token['uuid'],
|
||||
};
|
||||
|
||||
this.ApiService.revokeAppToken(null, params).then((resp) => {
|
||||
this.loadTokens();
|
||||
callback(true);
|
||||
}, errorHandler);
|
||||
}
|
||||
|
||||
private showToken(token) {
|
||||
const errorHandler = this.ApiService.errorDisplay('Could not find application token');
|
||||
const params = {
|
||||
'token_uuid': token['uuid'],
|
||||
};
|
||||
|
||||
this.ApiService.getAppToken(null, params).then((resp) => {
|
||||
this.tokenCredentials = {
|
||||
'title': resp['token']['title'],
|
||||
'namespace': this.UserService.currentUser().username,
|
||||
'username': '$app',
|
||||
'password': resp['token']['token_code'],
|
||||
};
|
||||
}, errorHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="col.bindModel.showRevokeToken(item)">
|
||||
<i class="fa fa-times"></i> Revoke Token
|
||||
</span>
|
||||
</span>
|
|
@ -0,0 +1 @@
|
|||
<time-ago datetime="item.created"></time-ago>
|
|
@ -0,0 +1 @@
|
|||
<expiration-status-view expiration-date="item.expiration"></expiration-status-view>
|
|
@ -0,0 +1 @@
|
|||
<time-ago datetime="item.last_accessed"></time-ago>
|
|
@ -0,0 +1 @@
|
|||
<a ng-click="col.bindModel.showToken(item)">{{ item.title }}</a>
|
|
@ -36,7 +36,7 @@ export class CorTableColumn implements OnInit {
|
|||
}
|
||||
|
||||
public processColumnForOrdered(value: any): any {
|
||||
if (this.kindof == 'datetime') {
|
||||
if (this.kindof == 'datetime' && value) {
|
||||
return this.tableService.getReversedTimestamp(value);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
<table class="co-table co-fixed-table" ng-show="$ctrl.tableData.length">
|
||||
<thead>
|
||||
<td ng-repeat="col in $ctrl.columns"
|
||||
ng-class="$ctrl.tablePredicateClass(col)" style="{{ ::col.style }}">
|
||||
ng-class="$ctrl.tablePredicateClass(col)" style="{{ ::col.style }}"
|
||||
class="{{ ::col.class }}">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ ::col.title }}</a>
|
||||
</td>
|
||||
</thead>
|
||||
|
|
|
@ -170,7 +170,7 @@ angular.module('quay').directive('credentialsDialog', function () {
|
|||
return '';
|
||||
}
|
||||
|
||||
return $scope.getEscapedUsername(credentials).toLowerCase() + '-pull-secret';
|
||||
return $scope.getSuffixedFilename(credentials, 'pull-secret');
|
||||
};
|
||||
|
||||
$scope.getKubernetesFile = function(credentials) {
|
||||
|
@ -193,8 +193,12 @@ angular.module('quay').directive('credentialsDialog', function () {
|
|||
return $scope.getSuffixedFilename(credentials, 'secret.yml')
|
||||
};
|
||||
|
||||
$scope.getEscapedUsername = function(credentials) {
|
||||
return credentials.username.replace(/[^a-zA-Z0-9]/g, '-');
|
||||
$scope.getEscaped = function(item) {
|
||||
var escaped = item.replace(/[^a-zA-Z0-9]/g, '-');
|
||||
if (escaped[0] == '-') {
|
||||
escaped = escaped.substr(1);
|
||||
}
|
||||
return escaped;
|
||||
};
|
||||
|
||||
$scope.getSuffixedFilename = function(credentials, suffix) {
|
||||
|
@ -202,7 +206,12 @@ angular.module('quay').directive('credentialsDialog', function () {
|
|||
return '';
|
||||
}
|
||||
|
||||
return $scope.getEscapedUsername(credentials) + '-' + suffix;
|
||||
var prefix = $scope.getEscaped(credentials.username);
|
||||
if (credentials.title) {
|
||||
prefix = $scope.getEscaped(credentials.title);
|
||||
}
|
||||
|
||||
return prefix + '-' + suffix;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -283,6 +283,9 @@ angular.module('quay').directive('logsView', function () {
|
|||
}
|
||||
},
|
||||
|
||||
'create_app_specific_token': 'Created external application token {app_specific_token_title}',
|
||||
'revoke_app_specific_token': 'Revoked external application token {app_specific_token_title}',
|
||||
|
||||
// Note: These are deprecated.
|
||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
||||
|
@ -345,7 +348,9 @@ angular.module('quay').directive('logsView', function () {
|
|||
'manifest_label_add': 'Add Manifest Label',
|
||||
'manifest_label_delete': 'Delete Manifest Label',
|
||||
'change_tag_expiration': 'Change tag expiration',
|
||||
|
||||
'create_app_specific_token': 'Create external app token',
|
||||
'revoke_app_specific_token': 'Revoke external app token',
|
||||
|
||||
// Note: these are deprecated.
|
||||
'add_repo_webhook': 'Add webhook',
|
||||
'delete_repo_webhook': 'Delete webhook'
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
<span ng-if="$ctrl.datetime" data-title="{{ $ctrl.datetime | amDateFormat:'llll' }}" bs-tooltip>
|
||||
<span am-time-ago="$ctrl.datetime"></span>
|
||||
</span>
|
||||
<span ng-if="!$ctrl.datetime">Unknown</span>
|
||||
<span ng-if="!$ctrl.datetime">Never</span>
|
||||
</span>
|
|
@ -39,6 +39,7 @@ import { CorTabsModule } from './directives/ui/cor-tabs/cor-tabs.module';
|
|||
import { TriggerDescriptionComponent } from './directives/ui/trigger-description/trigger-description.component';
|
||||
import { TimeAgoComponent } from './directives/ui/time-ago/time-ago.component';
|
||||
import { TimeDisplayComponent } from './directives/ui/time-display/time-display.component';
|
||||
import { AppSpecificTokenManagerComponent } from './directives/ui/app-specific-token-manager/app-specific-token-manager.component';
|
||||
import { MarkdownModule } from './directives/ui/markdown/markdown.module';
|
||||
import * as Clipboard from 'clipboard';
|
||||
|
||||
|
@ -83,6 +84,7 @@ import * as Clipboard from 'clipboard';
|
|||
TriggerDescriptionComponent,
|
||||
TimeAgoComponent,
|
||||
TimeDisplayComponent,
|
||||
AppSpecificTokenManagerComponent,
|
||||
],
|
||||
providers: [
|
||||
ViewArrayImpl,
|
||||
|
|
|
@ -70,25 +70,8 @@
|
|||
|
||||
<!-- Settings -->
|
||||
<cor-tab-pane id="settings">
|
||||
<!-- OIDC Token -->
|
||||
<div class="settings-section" ng-if="Config.AUTHENTICATION_TYPE == 'OIDC'">
|
||||
<h3>Docker CLI Token</h3>
|
||||
<div>
|
||||
A generated token is <strong>required</strong> to login via the Docker CLI.
|
||||
</div>
|
||||
|
||||
<table class="co-list-table" style="margin-top: 10px;">
|
||||
<tr>
|
||||
<td>CLI Token:</td>
|
||||
<td>
|
||||
<span class="external-login-button" is-link="true" action="cli" provider="oidcLoginProvider"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Encrypted Password -->
|
||||
<div class="settings-section" ng-if="Config.AUTHENTICATION_TYPE != 'OIDC'">
|
||||
<div class="settings-section" ng-if="Config.AUTHENTICATION_TYPE != 'AppToken'">
|
||||
<h3>Docker CLI Password</h3>
|
||||
<div ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
The Docker CLI stores passwords entered on the command line in <strong>plaintext</strong>. It is therefore highly recommended to generate an an encrypted version of your password to use for <code>docker login</code>.
|
||||
|
@ -109,6 +92,18 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<!-- App Specific tokens -->
|
||||
<div class="settings-section" ng-if="Features.APP_SPECIFIC_TOKENS">
|
||||
<h3>Docker CLI and other Application Tokens</h3>
|
||||
<div ng-if="Config.AUTHENTICATION_TYPE != 'AppToken'">
|
||||
As an alternative to using your password for Docker and rkt CLIs, an application token can be generated below.
|
||||
</div>
|
||||
<div ng-if="Config.AUTHENTICATION_TYPE == 'AppToken'">
|
||||
An application token is <strong>required</strong> to login via the Docker or rkt CLIs.
|
||||
</div>
|
||||
<app-specific-token-manager></app-specific-token-manager>
|
||||
</div>
|
||||
|
||||
<!-- User Settings -->
|
||||
<div class="settings-section">
|
||||
<h3>User Settings</h3>
|
||||
|
|
Reference in a new issue