First stab at token auth. The UI could use a little bit of polishing.
This commit is contained in:
parent
f1746417b1
commit
283f9b81ae
9 changed files with 360 additions and 91 deletions
78
auth/auth.py
78
auth/auth.py
|
@ -32,19 +32,34 @@ def process_basic_auth(auth):
|
||||||
credentials = b64decode(normalized[1]).split(':')
|
credentials = b64decode(normalized[1]).split(':')
|
||||||
|
|
||||||
if len(credentials) != 2:
|
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:
|
identity_changed.send(app, identity=Identity(token.code, 'token'))
|
||||||
logger.debug('Successfully validated user: %s' % authenticated.username)
|
return
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
ctx.authenticated_user = authenticated
|
|
||||||
|
|
||||||
new_identity = QuayDeferredPermissionUser(authenticated.username,
|
except model.DataModelException:
|
||||||
'username')
|
logger.debug('Invalid token: %s' % credentials[1])
|
||||||
identity_changed.send(app, identity=new_identity)
|
|
||||||
return
|
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.
|
# We weren't able to authenticate via basic auth.
|
||||||
logger.debug('Basic auth present but could not be validated.')
|
logger.debug('Basic auth present but could not be validated.')
|
||||||
|
@ -54,42 +69,37 @@ def process_basic_auth(auth):
|
||||||
def process_token(auth):
|
def process_token(auth):
|
||||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||||
if normalized[0].lower() != 'token' or len(normalized) != 2:
|
if normalized[0].lower() != 'token' or len(normalized) != 2:
|
||||||
logger.debug('Invalid token format.')
|
logger.debug('Not an auth token: %s' % auth)
|
||||||
return
|
return
|
||||||
|
|
||||||
token_details = normalized[1].split(',')
|
token_details = normalized[1].split(',')
|
||||||
|
|
||||||
if len(token_details) != 2:
|
if len(token_details) != 1:
|
||||||
logger.debug('Invalid token format.')
|
logger.warning('Invalid token format: %s' % auth)
|
||||||
return
|
abort(401)
|
||||||
|
|
||||||
token_vals = {val[0]: val[1] for val in
|
token_vals = {val[0]: val[1] for val in
|
||||||
(detail.split('=') for detail in token_details)}
|
(detail.split('=') for detail in token_details)}
|
||||||
if ('signature' not in token_vals or 'repository' not in token_vals):
|
if 'signature' not in token_vals:
|
||||||
logger.debug('Invalid token components.')
|
logger.warning('Token does not contain signature: %s' % auth)
|
||||||
return
|
abort(401)
|
||||||
|
|
||||||
unquoted = token_vals['repository'][1:-1]
|
try:
|
||||||
namespace, repository = parse_namespace_repository(unquoted)
|
token_data = model.load_token_data(token_vals['signature'])
|
||||||
logger.debug('Validing signature: %s' % token_vals['signature'])
|
|
||||||
validated = model.verify_token(token_vals['signature'], namespace,
|
|
||||||
repository)
|
|
||||||
|
|
||||||
if validated:
|
except model.InvalidTokenException:
|
||||||
session['repository'] = repository
|
logger.warning('Token could not be validated: %s' %
|
||||||
session['namespace'] = namespace
|
token_vals['signature'])
|
||||||
|
abort(401)
|
||||||
|
|
||||||
logger.debug('Successfully validated token: %s' % validated.code)
|
session['repository'] = token_data.repository.name
|
||||||
ctx = _request_ctx_stack.top
|
session['namespace'] = token_data.repository.namespace
|
||||||
ctx.validated_token = validated
|
|
||||||
|
|
||||||
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
|
identity_changed.send(app, identity=Identity(token_data.code, 'token'))
|
||||||
|
|
||||||
# WE weren't able to authenticate the token
|
|
||||||
logger.debug('Token present but could not be validated.')
|
|
||||||
abort(401)
|
|
||||||
|
|
||||||
|
|
||||||
def process_auth(f):
|
def process_auth(f):
|
||||||
|
|
|
@ -80,19 +80,14 @@ def on_identity_loaded(sender, identity):
|
||||||
identity_changed.send(app, identity=switch_to_deferred)
|
identity_changed.send(app, identity=switch_to_deferred)
|
||||||
|
|
||||||
elif identity.auth_type == 'token':
|
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)
|
repo_grant = _RepositoryNeed(token_data.repository.namespace,
|
||||||
|
token_data.repository.name,
|
||||||
if token.user:
|
token_data.role.name)
|
||||||
query = model.get_user_repo_permissions(token.user, token.repository)
|
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
|
||||||
for permission in query:
|
identity.provides.add(repo_grant)
|
||||||
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.')
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.error('Unknown identity auth type: %s' % identity.auth_type)
|
logger.error('Unknown identity auth type: %s' % identity.auth_type)
|
||||||
|
|
|
@ -99,10 +99,13 @@ def random_string_generator(length=16):
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(BaseModel):
|
class AccessToken(BaseModel):
|
||||||
code = CharField(default=random_string_generator(), unique=True, index=True)
|
friendly_name = CharField(null=True)
|
||||||
user = ForeignKeyField(User, null=True)
|
code = CharField(default=random_string_generator(length=64), unique=True,
|
||||||
|
index=True)
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository)
|
||||||
created = DateTimeField(default=datetime.now)
|
created = DateTimeField(default=datetime.now)
|
||||||
|
role = ForeignKeyField(Role)
|
||||||
|
temporary = BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class EmailConfirmation(BaseModel):
|
class EmailConfirmation(BaseModel):
|
||||||
|
|
|
@ -26,6 +26,10 @@ class InvalidPasswordException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidTokenException(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def create_user(username, password, email):
|
def create_user(username, password, email):
|
||||||
if not validate_email(email):
|
if not validate_email(email):
|
||||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||||
|
@ -159,25 +163,6 @@ def verify_user(username, password):
|
||||||
return None
|
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,
|
def get_visible_repositories(username=None, include_public=True, limit=None,
|
||||||
sort=False):
|
sort=False):
|
||||||
if not username and not include_public:
|
if not username and not include_public:
|
||||||
|
@ -485,3 +470,69 @@ def get_private_repo_count(username):
|
||||||
joined = Repository.select().join(Visibility)
|
joined = Repository.select().join(Visibility)
|
||||||
return joined.where(Repository.namespace == username,
|
return joined.where(Repository.namespace == username,
|
||||||
Visibility.name == 'private').count()
|
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)
|
||||||
|
|
|
@ -456,6 +456,92 @@ def delete_permissions(namespace, repository, username):
|
||||||
abort(403) # Permission denied
|
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):
|
def subscription_view(stripe_subscription, used_repos):
|
||||||
return {
|
return {
|
||||||
'currentPeriodStart': stripe_subscription.current_period_start,
|
'currentPeriodStart': stripe_subscription.current_period_start,
|
||||||
|
|
|
@ -19,25 +19,26 @@ from auth.permissions import (ModifyRepositoryPermission,
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def generate_headers(f):
|
def generate_headers(role='read'):
|
||||||
@wraps(f)
|
def decorator_method(f):
|
||||||
def wrapper(namespace, repository, *args, **kwargs):
|
@wraps(f)
|
||||||
response = f(namespace, repository, *args, **kwargs)
|
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:
|
if has_token_request:
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
token = model.create_access_token(get_authenticated_user(), repo)
|
token = model.create_access_token(repo, role)
|
||||||
token_str = 'signature=%s,repository="%s/%s"' % (token.code, namespace,
|
token_str = 'signature=%s' % token.code
|
||||||
repository)
|
response.headers['WWW-Authenticate'] = token_str
|
||||||
response.headers['WWW-Authenticate'] = token_str
|
response.headers['X-Docker-Token'] = token_str
|
||||||
response.headers['X-Docker-Token'] = token_str
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
return wrapper
|
return wrapper
|
||||||
|
return decorator_method
|
||||||
|
|
||||||
|
|
||||||
@app.route('/v1/users', methods=['POST'])
|
@app.route('/v1/users', methods=['POST'])
|
||||||
|
@ -47,6 +48,13 @@ def create_user():
|
||||||
username = user_data['username']
|
username = user_data['username']
|
||||||
password = user_data['password']
|
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)
|
existing_user = model.get_user(username)
|
||||||
if existing_user:
|
if existing_user:
|
||||||
verified = model.verify_user(username, password)
|
verified = model.verify_user(username, password)
|
||||||
|
@ -100,13 +108,17 @@ def update_user(username):
|
||||||
@app.route('/v1/repositories/<path:repository>', methods=['PUT'])
|
@app.route('/v1/repositories/<path:repository>', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers
|
@generate_headers(role='write')
|
||||||
def create_repository(namespace, repository):
|
def create_repository(namespace, repository):
|
||||||
image_descriptions = json.loads(request.data)
|
image_descriptions = json.loads(request.data)
|
||||||
|
|
||||||
repo = model.get_repository(namespace, repository)
|
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)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
if not permission.can():
|
if not permission.can():
|
||||||
abort(403)
|
abort(403)
|
||||||
|
@ -135,7 +147,10 @@ def create_repository(namespace, repository):
|
||||||
|
|
||||||
response = make_response('Created', 201)
|
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
|
return response
|
||||||
|
|
||||||
|
@ -143,7 +158,7 @@ def create_repository(namespace, repository):
|
||||||
@app.route('/v1/repositories/<path:repository>/images', methods=['PUT'])
|
@app.route('/v1/repositories/<path:repository>/images', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers
|
@generate_headers(role='write')
|
||||||
def update_images(namespace, repository):
|
def update_images(namespace, repository):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
|
@ -164,7 +179,7 @@ def update_images(namespace, repository):
|
||||||
@app.route('/v1/repositories/<path:repository>/images', methods=['GET'])
|
@app.route('/v1/repositories/<path:repository>/images', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers
|
@generate_headers(role='read')
|
||||||
def get_repository_images(namespace, repository):
|
def get_repository_images(namespace, repository):
|
||||||
permission = ReadRepositoryPermission(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'])
|
@app.route('/v1/repositories/<path:repository>/images', methods=['DELETE'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers
|
@generate_headers(role='write')
|
||||||
def delete_repository_images(namespace, repository):
|
def delete_repository_images(namespace, repository):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -469,6 +469,40 @@ 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.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) {
|
$scope.askChangeAccess = function(newAccess) {
|
||||||
$('#make' + newAccess + 'Modal').modal({});
|
$('#make' + newAccess + 'Modal').modal({});
|
||||||
};
|
};
|
||||||
|
@ -512,7 +546,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
||||||
repositoryFetch.get().then(function(repo) {
|
repositoryFetch.get().then(function(repo) {
|
||||||
$scope.repo = repo;
|
$scope.repo = repo;
|
||||||
$scope.loading = !($scope.permissions && $scope.repo);
|
$scope.loading = !($scope.permissions && $scope.repo && $scope.tokens);
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.permissions = null;
|
$scope.permissions = null;
|
||||||
$rootScope.title = 'Unknown Repository';
|
$rootScope.title = 'Unknown Repository';
|
||||||
|
@ -524,12 +558,23 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
permissionsFetch.get().then(function(resp) {
|
permissionsFetch.get().then(function(resp) {
|
||||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||||
$scope.permissions = resp.permissions;
|
$scope.permissions = resp.permissions;
|
||||||
$scope.loading = !($scope.permissions && $scope.repo);
|
$scope.loading = !($scope.permissions && $scope.repo && $scope.tokens);
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.permissions = null;
|
$scope.permissions = null;
|
||||||
$rootScope.title = 'Unknown Repository';
|
$rootScope.title = 'Unknown Repository';
|
||||||
$scope.loading = false;
|
$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) {
|
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
||||||
|
|
|
@ -56,7 +56,53 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
|
<!-- Token Permissions -->
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Access Token Permissions</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
|
||||||
|
<table class="permissions">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Token</td>
|
||||||
|
<td>Permissions</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr ng-repeat="(code, token) in tokens">
|
||||||
|
<td class="user">
|
||||||
|
<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">Delete</button></span>
|
||||||
|
<i class="icon-remove"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<form name="createTokenForm" ng-submit="createToken()">
|
||||||
|
<td>
|
||||||
|
<input class="form-control" placeholder="New token friendly name..." ng-model="newToken.friendlyName">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="submit" class="btn btn-sm btn-default">Create</button>
|
||||||
|
</td>
|
||||||
|
</form>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Public/Private -->
|
<!-- Public/Private -->
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -113,6 +159,24 @@
|
||||||
</div><!-- /.modal-content -->
|
</div><!-- /.modal-content -->
|
||||||
</div><!-- /.modal-dialog -->
|
</div><!-- /.modal-dialog -->
|
||||||
</div><!-- /.modal -->
|
</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">×</button>
|
||||||
|
<h4 class="modal-title">Token code for {{ shownToken.friendlyName }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{{ shownToken.code }}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
|
|
BIN
test.db
BIN
test.db
Binary file not shown.
Reference in a new issue