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
|
@ -382,6 +382,7 @@ def define_json_response(schema_name):
|
|||
return wrapper
|
||||
|
||||
|
||||
import endpoints.api.appspecifictokens
|
||||
import endpoints.api.billing
|
||||
import endpoints.api.build
|
||||
import endpoints.api.discovery
|
||||
|
|
112
endpoints/api/appspecifictokens.py
Normal file
112
endpoints/api/appspecifictokens.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
""" Manages app specific tokens for the current user. """
|
||||
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
|
||||
import features
|
||||
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from data import model
|
||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
||||
log_action, require_user_admin, require_fresh_login,
|
||||
path_param, NotFound, format_date, show_if)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def token_view(token, include_code=False):
|
||||
data = {
|
||||
'uuid': token.uuid,
|
||||
'title': token.title,
|
||||
'last_accessed': format_date(token.last_accessed),
|
||||
'created': format_date(token.created),
|
||||
'expiration': format_date(token.expiration),
|
||||
}
|
||||
|
||||
if include_code:
|
||||
data.update({
|
||||
'token_code': token.token_code,
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
@resource('/v1/user/apptoken')
|
||||
@show_if(features.APP_SPECIFIC_TOKENS)
|
||||
class AppTokens(ApiResource):
|
||||
""" Lists all app specific tokens for a user """
|
||||
schemas = {
|
||||
'NewToken': {
|
||||
'type': 'object',
|
||||
'required': [
|
||||
'title',
|
||||
],
|
||||
'properties': {
|
||||
'title': {
|
||||
'type': 'string',
|
||||
'description': 'The user-defined title for the token',
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@nickname('listAppTokens')
|
||||
def get(self):
|
||||
""" Lists the app specific tokens for the user. """
|
||||
tokens = model.appspecifictoken.list_tokens(get_authenticated_user())
|
||||
return {
|
||||
'tokens': [token_view(token, include_code=False) for token in tokens],
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@require_fresh_login
|
||||
@nickname('createAppToken')
|
||||
@validate_json_request('NewToken')
|
||||
def post(self):
|
||||
""" Create a new app specific token for user. """
|
||||
title = request.get_json()['title']
|
||||
token = model.appspecifictoken.create_token(get_authenticated_user(), title)
|
||||
|
||||
log_action('create_app_specific_token', get_authenticated_user().username,
|
||||
{'app_specific_token_title': token.title,
|
||||
'app_specific_token': token.uuid})
|
||||
|
||||
return {
|
||||
'token': token_view(token, include_code=True),
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/user/apptoken/<token_uuid>')
|
||||
@show_if(features.APP_SPECIFIC_TOKENS)
|
||||
@path_param('token_uuid', 'The uuid of the app specific token')
|
||||
class AppToken(ApiResource):
|
||||
""" Provides operations on an app specific token """
|
||||
@require_user_admin
|
||||
@require_fresh_login
|
||||
@nickname('getAppToken')
|
||||
def get(self, token_uuid):
|
||||
""" Returns a specific app token for the user. """
|
||||
token = model.appspecifictoken.get_token_by_uuid(token_uuid, owner=get_authenticated_user())
|
||||
if token is None:
|
||||
raise NotFound()
|
||||
|
||||
return {
|
||||
'token': token_view(token, include_code=True),
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@require_fresh_login
|
||||
@nickname('revokeAppToken')
|
||||
def delete(self, token_uuid):
|
||||
""" Revokes a specific app token for the user. """
|
||||
token = model.appspecifictoken.get_token_by_uuid(token_uuid, owner=get_authenticated_user())
|
||||
if token is None:
|
||||
raise NotFound()
|
||||
|
||||
model.appspecifictoken.revoke_token(token)
|
||||
|
||||
log_action('revoke_app_specific_token', get_authenticated_user().username,
|
||||
{'app_specific_token_title': token.title,
|
||||
'app_specific_token': token.uuid})
|
||||
|
||||
return '', 204
|
33
endpoints/api/test/test_appspecifictoken.py
Normal file
33
endpoints/api/test/test_appspecifictoken.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from endpoints.api.appspecifictokens import AppTokens, AppToken
|
||||
from endpoints.api.test.shared import conduct_api_call
|
||||
from endpoints.test.shared import client_with_identity
|
||||
from test.fixtures import *
|
||||
|
||||
def test_app_specific_tokens(app, client):
|
||||
with client_with_identity('devtable', client) as cl:
|
||||
# Add an app specific token.
|
||||
token_data = {'title': 'Testing 123'}
|
||||
resp = conduct_api_call(cl, AppTokens, 'POST', None, token_data, 200).json
|
||||
token_uuid = resp['token']['uuid']
|
||||
assert 'token_code' in resp['token']
|
||||
|
||||
# List the tokens and ensure we have the one added.
|
||||
resp = conduct_api_call(cl, AppTokens, 'GET', None, None, 200).json
|
||||
assert len(resp['tokens'])
|
||||
assert token_uuid in set([token['uuid'] for token in resp['tokens']])
|
||||
assert not set([token['token_code'] for token in resp['tokens'] if 'token_code' in token])
|
||||
|
||||
# Get the token and ensure we have its code.
|
||||
resp = conduct_api_call(cl, AppToken, 'GET', {'token_uuid': token_uuid}, None, 200).json
|
||||
assert resp['token']['uuid'] == token_uuid
|
||||
assert 'token_code' in resp['token']
|
||||
|
||||
# Delete the token.
|
||||
conduct_api_call(cl, AppToken, 'DELETE', {'token_uuid': token_uuid}, None, 204)
|
||||
|
||||
# Ensure the token no longer exists.
|
||||
resp = conduct_api_call(cl, AppTokens, 'GET', None, None, 200).json
|
||||
assert len(resp['tokens'])
|
||||
assert token_uuid not in set([token['uuid'] for token in resp['tokens']])
|
||||
|
||||
conduct_api_call(cl, AppToken, 'GET', {'token_uuid': token_uuid}, None, 404)
|
|
@ -13,6 +13,7 @@ from endpoints.api.signing import RepositorySignatures
|
|||
from endpoints.api.search import ConductRepositorySearch
|
||||
from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
|
||||
from endpoints.api.superuser import SuperUserRepositoryBuildStatus
|
||||
from endpoints.api.appspecifictokens import AppTokens, AppToken
|
||||
from endpoints.test.shared import client_with_identity, toggle_feature
|
||||
|
||||
from test.fixtures import *
|
||||
|
@ -22,9 +23,29 @@ BUILD_PARAMS = {'build_uuid': 'test-1234'}
|
|||
REPO_PARAMS = {'repository': 'devtable/someapp'}
|
||||
SEARCH_PARAMS = {'query': ''}
|
||||
NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'}
|
||||
|
||||
TOKEN_PARAMS = {'token_uuid': 'someuuid'}
|
||||
|
||||
@pytest.mark.parametrize('resource,method,params,body,identity,expected', [
|
||||
(AppTokens, 'GET', {}, {}, None, 401),
|
||||
(AppTokens, 'GET', {}, {}, 'freshuser', 200),
|
||||
(AppTokens, 'GET', {}, {}, 'reader', 200),
|
||||
(AppTokens, 'GET', {}, {}, 'devtable', 200),
|
||||
|
||||
(AppTokens, 'POST', {}, {}, None, 403),
|
||||
(AppTokens, 'POST', {}, {}, 'freshuser', 400),
|
||||
(AppTokens, 'POST', {}, {}, 'reader', 400),
|
||||
(AppTokens, 'POST', {}, {}, 'devtable', 400),
|
||||
|
||||
(AppToken, 'GET', TOKEN_PARAMS, {}, None, 401),
|
||||
(AppToken, 'GET', TOKEN_PARAMS, {}, 'freshuser', 404),
|
||||
(AppToken, 'GET', TOKEN_PARAMS, {}, 'reader', 404),
|
||||
(AppToken, 'GET', TOKEN_PARAMS, {}, 'devtable', 404),
|
||||
|
||||
(AppToken, 'DELETE', TOKEN_PARAMS, {}, None, 403),
|
||||
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'freshuser', 404),
|
||||
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'reader', 404),
|
||||
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'devtable', 404),
|
||||
|
||||
(OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, None, 403),
|
||||
(OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'freshuser', 403),
|
||||
(OrganizationTeamSyncing, 'POST', TEAM_PARAMS, {}, 'reader', 403),
|
||||
|
|
|
@ -1083,4 +1083,3 @@ class Users(ApiResource):
|
|||
abort(404)
|
||||
|
||||
return user_view(user)
|
||||
|
||||
|
|
Reference in a new issue