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

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

View 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

View 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)

View file

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

View file

@ -1083,4 +1083,3 @@ class Users(ApiResource):
abort(404)
return user_view(user)