Merge branch 'tokenauth'

Conflicts:
	test.db
This commit is contained in:
yackob03 2013-10-17 16:52:26 -04:00
commit 4f0dced8e7
12 changed files with 507 additions and 118 deletions

View file

@ -32,19 +32,34 @@ def process_basic_auth(auth):
credentials = b64decode(normalized[1]).split(':')
if len(credentials) != 2:
logger.debug('Invalid basic auth credential formet.')
logger.debug('Invalid basic auth credential format.')
authenticated = model.verify_user(credentials[0], credentials[1])
if credentials[0] == '$token':
# Use as token auth
try:
token = model.load_token_data(credentials[1])
logger.debug('Successfully validated token: %s' % credentials[1])
ctx = _request_ctx_stack.top
ctx.validated_token = token
if authenticated:
logger.debug('Successfully validated user: %s' % authenticated.username)
ctx = _request_ctx_stack.top
ctx.authenticated_user = authenticated
identity_changed.send(app, identity=Identity(token.code, 'token'))
return
new_identity = QuayDeferredPermissionUser(authenticated.username,
'username')
identity_changed.send(app, identity=new_identity)
return
except model.DataModelException:
logger.debug('Invalid token: %s' % credentials[1])
else:
authenticated = model.verify_user(credentials[0], credentials[1])
if authenticated:
logger.debug('Successfully validated user: %s' % authenticated.username)
ctx = _request_ctx_stack.top
ctx.authenticated_user = authenticated
new_identity = QuayDeferredPermissionUser(authenticated.username,
'username')
identity_changed.send(app, identity=new_identity)
return
# We weren't able to authenticate via basic auth.
logger.debug('Basic auth present but could not be validated.')
@ -54,42 +69,37 @@ def process_basic_auth(auth):
def process_token(auth):
normalized = [part.strip() for part in auth.split(' ') if part]
if normalized[0].lower() != 'token' or len(normalized) != 2:
logger.debug('Invalid token format.')
logger.debug('Not an auth token: %s' % auth)
return
token_details = normalized[1].split(',')
if len(token_details) != 2:
logger.debug('Invalid token format.')
return
if len(token_details) != 1:
logger.warning('Invalid token format: %s' % auth)
abort(401)
token_vals = {val[0]: val[1] for val in
(detail.split('=') for detail in token_details)}
if ('signature' not in token_vals or 'repository' not in token_vals):
logger.debug('Invalid token components.')
return
if 'signature' not in token_vals:
logger.warning('Token does not contain signature: %s' % auth)
abort(401)
unquoted = token_vals['repository'][1:-1]
namespace, repository = parse_namespace_repository(unquoted)
logger.debug('Validing signature: %s' % token_vals['signature'])
validated = model.verify_token(token_vals['signature'], namespace,
repository)
try:
token_data = model.load_token_data(token_vals['signature'])
if validated:
session['repository'] = repository
session['namespace'] = namespace
except model.InvalidTokenException:
logger.warning('Token could not be validated: %s' %
token_vals['signature'])
abort(401)
logger.debug('Successfully validated token: %s' % validated.code)
ctx = _request_ctx_stack.top
ctx.validated_token = validated
session['repository'] = token_data.repository.name
session['namespace'] = token_data.repository.namespace
identity_changed.send(app, identity=Identity(validated.code, 'token'))
logger.debug('Successfully validated token: %s' % token_data.code)
ctx = _request_ctx_stack.top
ctx.validated_token = token_data
return
# WE weren't able to authenticate the token
logger.debug('Token present but could not be validated.')
abort(401)
identity_changed.send(app, identity=Identity(token_data.code, 'token'))
def process_auth(f):

View file

@ -80,19 +80,14 @@ def on_identity_loaded(sender, identity):
identity_changed.send(app, identity=switch_to_deferred)
elif identity.auth_type == 'token':
logger.debug('Computing permissions for token: %s' % identity.id)
logger.debug('Loading permissions for token: %s' % identity.id)
token_data = model.load_token_data(identity.id)
token = model.get_token(identity.id)
if token.user:
query = model.get_user_repo_permissions(token.user, token.repository)
for permission in query:
t_grant = _RepositoryNeed(token.repository.namespace,
token.repository.name, permission.role.name)
logger.debug('Token added permission: {0}'.format(t_grant))
identity.provides.add(t_grant)
else:
logger.debug('Token was anonymous.')
repo_grant = _RepositoryNeed(token_data.repository.namespace,
token_data.repository.name,
token_data.role.name)
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
identity.provides.add(repo_grant)
else:
logger.error('Unknown identity auth type: %s' % identity.auth_type)

View file

@ -99,10 +99,13 @@ def random_string_generator(length=16):
class AccessToken(BaseModel):
code = CharField(default=random_string_generator(), unique=True, index=True)
user = ForeignKeyField(User, null=True)
friendly_name = CharField(null=True)
code = CharField(default=random_string_generator(length=64), unique=True,
index=True)
repository = ForeignKeyField(Repository)
created = DateTimeField(default=datetime.now)
role = ForeignKeyField(Role)
temporary = BooleanField(default=True)
class EmailConfirmation(BaseModel):

View file

@ -26,6 +26,10 @@ class InvalidPasswordException(DataModelException):
pass
class InvalidTokenException(DataModelException):
pass
def create_user(username, password, email):
if not validate_email(email):
raise InvalidEmailAddressException('Invalid email address: %s' % email)
@ -159,25 +163,6 @@ def verify_user(username, password):
return None
def create_access_token(user, repository):
new_token = AccessToken.create(user=user, repository=repository)
return new_token
def verify_token(code, namespace_name, repository_name):
joined = AccessToken.select(AccessToken, Repository).join(Repository)
tokens = list(joined.where(AccessToken.code == code,
Repository.namespace == namespace_name,
Repository.name == repository_name))
if tokens:
return tokens[0]
return None
def get_token(code):
return AccessToken.get(AccessToken.code == code)
def get_visible_repositories(username=None, include_public=True, limit=None,
sort=False):
if not username and not include_public:
@ -485,3 +470,69 @@ def get_private_repo_count(username):
joined = Repository.select().join(Visibility)
return joined.where(Repository.namespace == username,
Visibility.name == 'private').count()
def create_access_token(repository, role):
role = Role.get(Role.name == role)
new_token = AccessToken.create(repository=repository, temporary=True,
role=role)
return new_token
def create_delegate_token(namespace_name, repository_name, friendly_name):
read_only = Role.get(name='read')
repo = Repository.get(Repository.name == repository_name,
Repository.namespace == namespace_name)
new_token = AccessToken.create(repository=repo, role=read_only,
friendly_name=friendly_name, temporary=False)
return new_token
def get_repository_delegate_tokens(namespace_name, repository_name):
selected = AccessToken.select(AccessToken, Role)
with_repo = selected.join(Repository)
with_role = with_repo.switch(AccessToken).join(Role)
return with_role.where(Repository.name == repository_name,
Repository.namespace == namespace_name,
AccessToken.temporary == False)
def get_repo_delegate_token(namespace_name, repository_name, code):
repo_query = get_repository_delegate_tokens(namespace_name, repository_name)
found = list(repo_query.where(AccessToken.code == code))
if found:
return found[0]
else:
raise InvalidTokenException('Unable to find token with code: %s' % code)
def set_repo_delegate_token_role(namespace_name, repository_name, code, role):
token = get_repo_delegate_token(namespace_name, repository_name, code)
if role != 'read' and role != 'write':
raise DataModelException('Invalid role for delegate token: %s' % role)
new_role = Role.get(Role.name == role)
token.role = new_role
token.save()
return token
def delete_delegate_token(namespace_name, repository_name, code):
token = get_repo_delegate_token(namespace_name, repository_name, code)
token.delete_instance()
def load_token_data(code):
""" Load the permissions for any token by code. """
selected = AccessToken.select(AccessToken, Repository, Role)
with_role = selected.join(Role)
with_repo = with_role.switch(AccessToken).join(Repository)
fetched = list(with_repo.where(AccessToken.code == code))
if fetched:
return fetched[0]
else:
raise InvalidTokenException('Invalid delegate token code: %s' % code)

View file

@ -456,6 +456,92 @@ def delete_permissions(namespace, repository, username):
abort(403) # Permission denied
def token_view(token_obj):
return {
'friendlyName': token_obj.friendly_name,
'code': token_obj.code,
'role': token_obj.role.name,
}
@app.route('/api/repository/<path:repository>/tokens/', methods=['GET'])
@api_login_required
@parse_repository_name
def list_repo_tokens(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
tokens = model.get_repository_delegate_tokens(namespace, repository)
return jsonify({
'tokens': {token.code: token_view(token) for token in tokens}
})
abort(403) # Permission denied
@app.route('/api/repository/<path:repository>/tokens/<code>', methods=['GET'])
@api_login_required
@parse_repository_name
def get_tokens(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
perm = model.get_repo_delegate_token(namespace, repository, code)
return jsonify(token_view(perm))
abort(403) # Permission denied
@app.route('/api/repository/<path:repository>/tokens/', methods=['POST'])
@api_login_required
@parse_repository_name
def create_token(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
token_params = request.get_json()
token = model.create_delegate_token(namespace, repository,
token_params['friendlyName'])
resp = jsonify(token_view(token))
resp.status_code = 201
return resp
abort(403) # Permission denied
@app.route('/api/repository/<path:repository>/tokens/<code>', methods=['PUT'])
@api_login_required
@parse_repository_name
def change_token(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
new_permission = request.get_json()
logger.debug('Setting permission to: %s for code %s' %
(new_permission['role'], code))
token = model.set_repo_delegate_token_role(namespace, repository, code,
new_permission['role'])
resp = jsonify(token_view(token))
return resp
abort(403) # Permission denied
@app.route('/api/repository/<path:repository>/tokens/<code>',
methods=['DELETE'])
@api_login_required
@parse_repository_name
def delete_token(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_delegate_token(namespace, repository, code)
return make_response('Deleted', 204)
abort(403) # Permission denied
def subscription_view(stripe_subscription, used_repos):
return {
'currentPeriodStart': stripe_subscription.current_period_start,

View file

@ -19,25 +19,26 @@ from auth.permissions import (ModifyRepositoryPermission,
logger = logging.getLogger(__name__)
def generate_headers(f):
@wraps(f)
def wrapper(namespace, repository, *args, **kwargs):
response = f(namespace, repository, *args, **kwargs)
def generate_headers(role='read'):
def decorator_method(f):
@wraps(f)
def wrapper(namespace, repository, *args, **kwargs):
response = f(namespace, repository, *args, **kwargs)
response.headers['X-Docker-Endpoints'] = app.config['REGISTRY_SERVER']
response.headers['X-Docker-Endpoints'] = app.config['REGISTRY_SERVER']
has_token_request = request.headers.get('X-Docker-Token', '')
has_token_request = request.headers.get('X-Docker-Token', '')
if has_token_request:
repo = model.get_repository(namespace, repository)
token = model.create_access_token(get_authenticated_user(), repo)
token_str = 'signature=%s,repository="%s/%s"' % (token.code, namespace,
repository)
response.headers['WWW-Authenticate'] = token_str
response.headers['X-Docker-Token'] = token_str
if has_token_request:
repo = model.get_repository(namespace, repository)
token = model.create_access_token(repo, role)
token_str = 'signature=%s' % token.code
response.headers['WWW-Authenticate'] = token_str
response.headers['X-Docker-Token'] = token_str
return response
return wrapper
return response
return wrapper
return decorator_method
@app.route('/v1/users', methods=['POST'])
@ -47,6 +48,13 @@ def create_user():
username = user_data['username']
password = user_data['password']
if username == '$token':
try:
token = model.load_token_data(password)
return make_response('Verified', 201)
except model.InvalidTokenException:
abort(401)
existing_user = model.get_user(username)
if existing_user:
verified = model.verify_user(username, password)
@ -100,13 +108,17 @@ def update_user(username):
@app.route('/v1/repositories/<path:repository>', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers
@generate_headers(role='write')
def create_repository(namespace, repository):
image_descriptions = json.loads(request.data)
repo = model.get_repository(namespace, repository)
if repo:
if not repo and get_authenticated_user() is None:
logger.debug('Attempt to create new repository with token auth.')
abort(400)
elif repo:
permission = ModifyRepositoryPermission(namespace, repository)
if not permission.can():
abort(403)
@ -135,7 +147,10 @@ def create_repository(namespace, repository):
response = make_response('Created', 201)
mixpanel.track(get_authenticated_user().username, 'push_repo')
if get_authenticated_user():
mixpanel.track(get_authenticated_user().username, 'push_repo')
else:
mixpanel.track(get_validated_token().code, 'push_repo')
return response
@ -143,7 +158,7 @@ def create_repository(namespace, repository):
@app.route('/v1/repositories/<path:repository>/images', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers
@generate_headers(role='write')
def update_images(namespace, repository):
permission = ModifyRepositoryPermission(namespace, repository)
@ -164,7 +179,7 @@ def update_images(namespace, repository):
@app.route('/v1/repositories/<path:repository>/images', methods=['GET'])
@process_auth
@parse_repository_name
@generate_headers
@generate_headers(role='read')
def get_repository_images(namespace, repository):
permission = ReadRepositoryPermission(namespace, repository)
@ -196,7 +211,7 @@ def get_repository_images(namespace, repository):
@app.route('/v1/repositories/<path:repository>/images', methods=['DELETE'])
@process_auth
@parse_repository_name
@generate_headers
@generate_headers(role='write')
def delete_repository_images(namespace, repository):
pass

View file

@ -2,11 +2,48 @@
font-family: 'Droid Sans', sans-serif;
}
.description-overview {
padding: 4px;
font-size: 16px;
}
.description-list {
margin: 10px;
padding: 0px;
}
.description-list li:before {
content: "\00BB";
margin-right: 6px;
font-size: 18px;
}
.description-list li {
list-style-type: none;
margin: 0px;
padding: 6px;
}
.info-icon {
display: inline-block;
float: right;
vertical-align: middle;
font-size: 20px;
}
.accordion-toggle {
cursor: pointer;
text-decoration: none !important;
}
.user-guide h3 {
margin-bottom: 20px;
}
.user-guide h3 .label {
float: right;
}
.plans .callout {
font-size: 2em;
text-align: center;
@ -441,7 +478,8 @@ p.editable:hover i {
}
.repo dl.dl-horizontal dt {
width: 60px;
width: 80px;
padding-right: 10px;
}
.repo dl.dl-horizontal dd {
@ -485,18 +523,21 @@ p.editable:hover i {
color: white;
}
.repo #clipboardCopied {
.repo #clipboardCopied.hovering {
position: absolute;
right: 0px;
top: 40px;
}
.repo #clipboardCopied {
font-size: 0.8em;
display: inline-block;
margin-right: 10px;
background: black;
color: white;
padding: 6px;
border-radius: 4px;
-webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards;
-moz-animation: fadeOut 4s ease-in-out 0s 1 forwards;
@ -557,6 +598,18 @@ p.editable:hover i {
padding-left: 36px;
}
.repo-admin .token-dialog-body .well {
margin-bottom: 0px;
}
.repo-admin .token-view {
background: transparent;
display: block;
border: 0px transparent;
font-size: 12px;
width: 100%;
}
.repo-admin .panel {
display: inline-block;
width: 620px;
@ -571,6 +624,10 @@ p.editable:hover i {
min-width: 300px;
}
.repo-admin .token a {
cursor: pointer;
}
.repo .description p {
margin-bottom: 6px;
}

View file

@ -1,3 +1,17 @@
$.fn.clipboardCopy = function() {
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function() {
// Resets the animation.
var elem = $('#clipboardCopied')[0];
elem.style.display = 'none';
// Show the notification.
setTimeout(function() {
elem.style.display = 'inline-block';
}, 1);
});
};
function getFirstTextLine(commentString) {
if (!commentString) { return; }
@ -398,18 +412,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location) {
$scope.setTag($routeParams.tag);
var clip = new ZeroClipboard($('#copyClipboard'), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function() {
// Resets the animation.
var elem = $('#clipboardCopied')[0];
elem.style.display = 'none';
// Show the notification.
setTimeout(function() {
elem.style.display = 'block';
}, 1);
});
$('#copyClipboard').clipboardCopy();
$scope.loading = false;
}, function() {
$scope.repo = null;
@ -422,6 +425,13 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location) {
}
function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
$('.info-icon').popover({
'trigger': 'hover',
'html': true
});
$('#copyClipboard').clipboardCopy();
var namespace = $routeParams.namespace;
var name = $routeParams.name;
@ -513,6 +523,42 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
});
};
$scope.createToken = function() {
var friendlyName = {
'friendlyName': $scope.newToken.friendlyName
};
var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/tokens/');
permissionPost.customPOST(friendlyName).then(function(newToken) {
$scope.newToken.friendlyName = '';
$scope.createTokenForm.$setPristine();
$scope.tokens[newToken.code] = newToken;
});
};
$scope.deleteToken = function(tokenCode) {
var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode);
deleteAction.customDELETE().then(function() {
delete $scope.tokens[tokenCode];
});
};
$scope.changeTokenAccess = function(tokenCode, newAccess) {
var role = {
'role': newAccess
};
var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode);
deleteAction.customPUT(role).then(function(updated) {
$scope.tokens[updated.code] = updated;
});
};
$scope.showToken = function(tokenCode) {
$scope.shownToken = $scope.tokens[tokenCode];
$('#tokenmodal').modal({});
};
$scope.askChangeAccess = function(newAccess) {
$('#make' + newAccess + 'Modal').modal({});
};
@ -556,7 +602,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
repositoryFetch.get().then(function(repo) {
$scope.repo = repo;
$scope.loading = !($scope.permissions && $scope.repo);
$scope.loading = !($scope.permissions && $scope.repo && $scope.tokens);
}, function() {
$scope.permissions = null;
$rootScope.title = 'Unknown Repository';
@ -568,12 +614,23 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
permissionsFetch.get().then(function(resp) {
$rootScope.title = 'Settings - ' + namespace + '/' + name;
$scope.permissions = resp.permissions;
$scope.loading = !($scope.permissions && $scope.repo);
$scope.loading = !($scope.permissions && $scope.repo && $scope.tokens);
}, function() {
$scope.permissions = null;
$rootScope.title = 'Unknown Repository';
$scope.loading = false;
});
// Fetch the tokens.
var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/');
tokensFetch.get().then(function(resp) {
$scope.tokens = resp.tokens;
$scope.loading = !($scope.permissions && $scope.repo && $scope.tokens);
}, function() {
$scope.tokens = null;
$scope.loading = false;
});
}
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {

View file

@ -1,10 +1,20 @@
<div class="container ready-indicator" data-status="{{ status }}">
<div class="alert alert-warning">Warning: Quay requires docker version 0.6.2 or higher to work</div>
<h2>Getting started guide</h2>
<div class="container">
<h2>User guide</h2>
<div class="user-guide container">
<h3>Pushing a repository to Quay</h3>
<h3>Pulling a repository from Quay</h3>
<div class="container">
<div class="alert alert-info">Note: <b>Private</b> repositories require you to be <b>logged in</b> or the pull will fail. See below for how to sign into Quay if you have never done so before. </div>
To pull a repository from Quay, run the following command:
<br><br>
<pre>docker pull quay.io/<i>username/repo_name</i></pre>
</div>
<br>
<h3>Pushing a repository to Quay <span class="label label-success">Requires Write Access</span></h3>
<div class="container">
First, tag the image with your repository name:<br><br>
<pre>docker tag <i>0u123imageid</i> quay.io/<i>username/repo_name</i></pre>
@ -14,12 +24,41 @@
</div>
<br>
<h3>Pulling a repository from Quay</h3>
<h3>Granting and managing permissions to users <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="alert alert-info">Note: <b>Private</b> repositories require you to be <b>logged in</b> or the pull will fail. See below for how to sign into Quay if you have never done so before. </div>
To pull a repository from Quay, run the following command:
<br><br>
<pre>docker pull quay.io/<i>username/repo_name</i></pre>
<div class="description-overview">Quay allows a repository to be shared any number of users and to grant those users any level of permissions for a repository</div>
<ul class="description-list">
<li>Permissions for a repository can be granted and managed in the repository's admin interface
<li><b>Adding a user:</b> Type that user's username in the "Add New User..." field, and select the user
<li><b>Changing permissions:</b> A user's permissions (read, read/write or admin) can be changed by clicking the field to the right of the user
<li><b>Removing a user:</b> A user can be removed from the list by clicking the <b>X</b> and then clicking <b>Delete</b>
</ul>
</div>
<br>
<h3>Using access tokens in place of users <span class="label label-info">Requires Admin Access</span></h3>
<div class="container">
<div class="description-overview">
There are many circumstances where it makes sense to <b>not</b> use a user's username and password (deployment scripts, etc).
To support this case, Quay allows the use of <b>access tokens</b> which can be created on a repository and have read and/or write
permissions, without any passwords.
</div>
<ul class="description-list">
<li>Tokens can be managed in the repository's admin interface
<li><b>Adding a token:</b> Enter a user-readable description in the "New token description" field
<li><b>Changing permissions:</b> A token's permissions (read or read/write) can be changed by clicking the field to the right of the token
<li><b>Deleting a token:</b> A token can be deleted by clicking the <b>X</b> and then clicking <b>Delete</b>
<li><b>Using a token:</b> To use the token, the following credentials can be used:
<dl class="dl-horizontal">
<dt>Username</dt><dd>$token</dd>
<dt>Password</dt><dd>(token value can be found by clicking on the token)</dd>
<dt>Email</dt><dd>This value is ignored, any value may be used.</dd>
</dl>
</ul>
</div>
<br>

View file

@ -15,7 +15,7 @@
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li><a ng-href="/repository/">Repositories</a></li>
<li><a ng-href="/guide/">Getting Started</a></li>
<li><a ng-href="/guide/">User Guide</a></li>
<li><a ng-href="/plans/">Plans &amp; Pricing</a></li>
</ul>
@ -50,4 +50,4 @@
<a href="/signin/">Sign in</a>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.navbar-collapse -->

View file

@ -16,7 +16,10 @@
<!-- User Access Permissions -->
<div class="panel panel-default">
<div class="panel-heading">User Access Permissions</div>
<div class="panel-heading">User Access Permissions
<i class="info-icon icon-info-sign" data-placement="left" data-content="Allow any number of users to read, write or administer this repository"></i>
</div>
<div class="panel-body">
<table class="permissions">
@ -56,7 +59,55 @@
</table>
</div>
</div>
<br>
<!-- Token Permissions -->
<div class="panel panel-default">
<div class="panel-heading">Access Token Permissions
<i class="info-icon icon-info-sign" data-placement="left" data-content="Grant permissions to this repository by creating unique tokens that can be used without entering account passwords<br><br>To use in docker:<br><dl class='dl-horizontal'><dt>Username</dt><dd>$token</dd><dt>Password</dt><dd>(token value)</dd><dt>Email</dt><dd>(any value)</dd></dl>"></i>
</div>
<div class="panel-body">
<form name="createTokenForm" ng-submit="createToken()">
<table class="permissions">
<thead>
<tr>
<td>Token Description</td>
<td>Permissions</td>
<td></td>
</tr>
</thead>
<tr ng-repeat="(code, token) in tokens">
<td class="user token">
<i class="icon-key"></i>
<a ng-click="showToken(token.code)">{{ token.friendlyName }}</a>
</td>
<td class="user-permissions">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default" ng-click="changeTokenAccess(token.code, 'read')" ng-class="{read: 'active', write: ''}[token.role]">Read only</button>
<button type="button" class="btn btn-default" ng-click="changeTokenAccess(token.code, 'write')" ng-class="{read: '', write: 'active'}[token.role]">Write</button>
</div>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Delete Token">
<span class="delete-ui-button" ng-click="deleteToken(token.code)"><button class="btn btn-danger" type="button">Delete</button></span>
<i class="icon-remove"></i>
</span>
</td>
</tr>
<tr>
<td>
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName"required>
</td>
<td>
<button type="submit" ng-disabled="createTokenForm.$invalid" class="btn btn-sm btn-default">Create</button>
</td>
</tr>
</table>
</form>
</div>
</div>
<!-- Public/Private -->
<div class="panel panel-default">
@ -113,6 +164,31 @@
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="tokenmodal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"><i class="icon-key"></i> {{ shownToken.friendlyName }}</h4>
</div>
<div class="modal-body token-dialog-body">
<div class="alert alert-info">The docker <u>username</u> is <b>$token</b> and the <u>password</u> is the token. You may use any value for email.</div>
<div class="well well-sm">
<input id="token-view" class="token-view" type="text" value="{{ shownToken.code }}" onClick="this.select();" readonly>
</div>
</div>
<div class="modal-footer">
<div id="clipboardCopied" style="display: none">
Copied to clipboard
</div>
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->

View file

@ -37,7 +37,7 @@
</div>
</div>
<div id="clipboardCopied" style="display: none">
<div id="clipboardCopied" class="hovering" style="display: none">
Copied to clipboard
</div>
</div>