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:
Joseph Schorr 2017-12-08 17:05:59 -05:00
parent 53b762a875
commit 524d77f527
50 changed files with 943 additions and 289 deletions

View file

@ -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">

View file

@ -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) {

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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>

View file

@ -0,0 +1 @@
<time-ago datetime="item.created"></time-ago>

View file

@ -0,0 +1 @@
<expiration-status-view expiration-date="item.expiration"></expiration-status-view>

View file

@ -0,0 +1 @@
<time-ago datetime="item.last_accessed"></time-ago>

View file

@ -0,0 +1 @@
<a ng-click="col.bindModel.showToken(item)">{{ item.title }}</a>

View file

@ -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);
}

View file

@ -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>

View file

@ -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;
};
}
};

View file

@ -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'

View file

@ -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>

View file

@ -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,

View file

@ -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>