From fb1dca4e94915f55703c672aab4f825cfe9f96dc Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 5 Apr 2016 15:27:45 -0400 Subject: [PATCH] Add API usage tests --- data/model/service_keys.py | 2 +- endpoints/api/superuser.py | 51 ++++++++++++---- test/test_api_security.py | 39 ++++++++++-- test/test_api_usage.py | 122 ++++++++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 20 deletions(-) diff --git a/data/model/service_keys.py b/data/model/service_keys.py index 7da5bf8d6..c4dad1828 100644 --- a/data/model/service_keys.py +++ b/data/model/service_keys.py @@ -123,7 +123,7 @@ def approve_service_key(kid, approver, approval_type, notes=''): delete_all_notifications_by_path_prefix('/service_key_approval/{0}'.format(kid)) _gc_expired(key.service) - + return key def _list_service_keys_query(kid=None, service=None, approved_only=False): query = ServiceKey.select().join(ServiceKeyApproval, JOIN_LEFT_OUTER) diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 0312a27ea..ab91bda00 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -5,7 +5,6 @@ import logging import os import string -from calendar import timegm from datetime import datetime from hashlib import sha256 from random import SystemRandom @@ -600,8 +599,8 @@ class SuperUserServiceKeyManagement(ApiResource): 'auto_approved': True, } - log_action('service_key_create', user.username, key_log_metadata) - log_action('service_key_approve', user.username, key_log_metadata) + log_action('service_key_create', None, key_log_metadata) + log_action('service_key_approve', None, key_log_metadata) return jsonify({ 'kid': kid, @@ -616,7 +615,7 @@ class SuperUserServiceKeyManagement(ApiResource): @resource('/v1/superuser/keys/') @path_param('kid', 'The unique identifier for a service key') @show_if(features.SUPER_USERS) -class SuperUserServiceKeyUpdater(ApiResource): +class SuperUserServiceKey(ApiResource): """ Resource for managing service keys. """ schemas = { 'PutServiceKey': { @@ -640,6 +639,19 @@ class SuperUserServiceKeyUpdater(ApiResource): }, } + @verify_not_prod + @nickname('getServiceKey') + @require_scope(scopes.SUPERUSER) + def get(self, kid): + if SuperUserPermission().can(): + try: + key = model.service_keys.get_service_key(kid) + return jsonify(key_view(key)) + except model.service_keys.ServiceKeyDoesNotExist: + abort(404) + + abort(403) + @require_fresh_login @verify_not_prod @nickname('updateServiceKey') @@ -675,13 +687,13 @@ class SuperUserServiceKeyUpdater(ApiResource): 'expiration_date': expiration_date, }) - log_action('service_key_extend', user.username, key_log_metadata) + log_action('service_key_extend', None, key_log_metadata) model.service_keys.set_key_expiration(kid, expiration_date) if 'name' in body or 'metadata' in body: model.service_keys.update_service_key(kid, body.get('name'), body.get('metadata')) - log_action('service_key_modify', user.username, key_log_metadata) + log_action('service_key_modify', None, key_log_metadata) return jsonify(key_view(model.service_keys.get_service_key(kid))) @@ -693,7 +705,10 @@ class SuperUserServiceKeyUpdater(ApiResource): @require_scope(scopes.SUPERUSER) def delete(self, kid): if SuperUserPermission().can(): - key = model.service_keys.delete_service_key(kid) + try: + key = model.service_keys.delete_service_key(kid) + except model.service_keys.ServiceKeyDoesNotExist: + abort(404) key_log_metadata = { 'kid': kid, @@ -703,9 +718,8 @@ class SuperUserServiceKeyUpdater(ApiResource): 'expiration_date': key.expiration_date, } - user = get_authenticated_user() - log_action('service_key_delete', user.username, key_log_metadata) - return make_response('', 201) + log_action('service_key_delete', None, key_log_metadata) + return make_response('', 204) abort(403) @@ -734,13 +748,24 @@ class SuperUserServiceKeyApproval(ApiResource): @verify_not_prod @nickname('approveServiceKey') @require_scope(scopes.SUPERUSER) - def put(self, kid): + @validate_json_request('ApproveServiceKey') + def post(self, kid): if SuperUserPermission().can(): notes = request.get_json().get('notes', '') approver = get_authenticated_user() try: - model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER, - notes=notes) + key = model.service_keys.approve_service_key(kid, approver, ServiceKeyApprovalType.SUPERUSER, + notes=notes) + + # Log the approval of the service key. + key_log_metadata = { + 'kid': kid, + 'service': key.service, + 'name': key.name, + 'expiration_date': key.expiration_date, + } + + log_action('service_key_approve', None, key_log_metadata) except model.ServiceKeyDoesNotExist: abort(404) except model.ServiceKeyAlreadyApproved: diff --git a/test/test_api_security.py b/test/test_api_security.py index e6385fd92..b73c27bf5 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -49,7 +49,7 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana SuperUserSendRecoveryEmail, ChangeLog, SuperUserOrganizationManagement, SuperUserOrganizationList, SuperUserAggregateLogs, SuperUserServiceKeyManagement, - SuperUserServiceKeyUpdater, SuperUserServiceKeyApproval) + SuperUserServiceKey, SuperUserServiceKeyApproval) from endpoints.api.secscan import RepositoryImageSecurity @@ -3912,6 +3912,25 @@ class TestSuperUserSendRecoveryEmail(ApiTestCase): self._run_test('POST', 404, 'devtable', None) +class TestSuperUserServiceKeyApproval(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserServiceKeyApproval, kid=1234) + + def test_post_anonymous(self): + self._run_test('POST', 401, None, {}) + + def test_post_freshuser(self): + self._run_test('POST', 403, 'freshuser', {}) + + def test_post_reader(self): + self._run_test('POST', 403, 'reader', {}) + + def test_post_devtable(self): + self._run_test('POST', 404, 'devtable', {}) + + + class TestSuperUserServiceKeyManagement(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) @@ -3942,10 +3961,22 @@ class TestSuperUserServiceKeyManagement(ApiTestCase): self._run_test('POST', 200, 'devtable', dict(service='someservice', expiration=None)) -class TestSuperUserServiceKeyUpdater(ApiTestCase): +class TestSuperUserServiceKey(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) - self._set_url(SuperUserServiceKeyUpdater, kid=1234) + self._set_url(SuperUserServiceKey, kid=1234) + + def test_get_anonymous(self): + self._run_test('GET', 403, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 404, 'devtable', None) def test_delete_anonymous(self): self._run_test('DELETE', 401, None, None) @@ -3957,7 +3988,7 @@ class TestSuperUserServiceKeyUpdater(ApiTestCase): self._run_test('DELETE', 403, 'reader', None) def test_delete_devtable(self): - self._run_test('DELETE', 400, 'devtable', None) + self._run_test('DELETE', 404, 'devtable', None) def test_put_anonymous(self): self._run_test('PUT', 401, None, {}) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 7f6e69c7c..29bf30b88 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -6,12 +6,15 @@ import logging import re import json as py_json +from calendar import timegm from StringIO import StringIO from urllib import urlencode from urlparse import urlparse, urlunparse, parse_qs from playhouse.test_utils import assert_query_count, _QueryLogHandler from httmock import urlmatch, HTTMock +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend from endpoints.api import api_bp, api from endpoints.building import PreparedBuild @@ -20,7 +23,7 @@ from app import app, config_provider from buildtrigger.basehandler import BuildTriggerHandler from initdb import setup_database_for_testing, finished_database_for_testing from data import database, model -from data.database import RepositoryActionCount +from data.database import RepositoryActionCount, LogEntry, LogEntryKind from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags @@ -53,7 +56,9 @@ from endpoints.api.organization import (OrganizationList, OrganizationMember, from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission, RepositoryTeamPermissionList, RepositoryUserPermissionList) -from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement +from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement, + SuperUserServiceKeyManagement, SuperUserServiceKey, + SuperUserServiceKeyApproval) from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile, SuperUserCreateInitialSuperUser) @@ -3554,6 +3559,119 @@ class TestRepositoryImageSecurity(ApiTestCase): self.assertEquals(1, response['data']['Layer']['IndexedByVersion']) +class TestSuperUserKeyManagement(ApiTestCase): + def test_get_update_keys(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse(SuperUserServiceKeyManagement) + self.assertEquals(3, len(json['keys'])) + + key = json['keys'][0] + self.assertTrue('name' in key) + self.assertTrue('service' in key) + self.assertTrue('kid' in key) + self.assertTrue('created_date' in key) + self.assertTrue('expiration_date' in key) + self.assertTrue('jwk' in key) + self.assertTrue('approval' in key) + self.assertTrue('metadata' in key) + + # Update the key's name. + self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']), + data=dict(name='somenewname')) + + # Ensure the key's name has been changed. + json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) + self.assertEquals('somenewname', json['name']) + + # Ensure a log was added for the modification. + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_modify') + self.assertEquals(1, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) + + # Update the key's metadata. + self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']), + data=dict(metadata=dict(foo='bar'))) + + # Ensure the key's metadata has been changed. + json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) + self.assertEquals('bar', json['metadata']['foo']) + + # Ensure a log was added for the modification. + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_modify') + self.assertEquals(2, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) + + # Change the key's expiration. + self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']), + data=dict(expiration=None)) + + # Ensure the key's expiration has been changed. + json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) + self.assertIsNone(json['expiration_date']) + + # Ensure a log was added for the modification. + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_extend') + self.assertEquals(1, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) + + # Delete the key. + self.deleteResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) + + # Ensure the key no longer exists. + self.getResponse(SuperUserServiceKey, params=dict(kid=key['kid']), expected_code=404) + + json = self.getJsonResponse(SuperUserServiceKeyManagement) + self.assertEquals(2, len(json['keys'])) + + # Ensure a log was added for the deletion. + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_delete') + self.assertEquals(1, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) + + + def test_create_key(self): + self.login(ADMIN_ACCESS_USER) + + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_create') + existing_log_count = model.log.LogEntry.select().where(LogEntry.kind == kind).count() + + new_key = { + 'service': 'coolservice', + 'name': 'mynewkey', + 'metadata': dict(foo='baz'), + 'notes': 'whazzup!?', + 'expiration': timegm((datetime.datetime.now() + datetime.timedelta(days=1)).utctimetuple()), + } + + # Create the key. + json = self.postJsonResponse(SuperUserServiceKeyManagement, data=new_key) + self.assertEquals('mynewkey', json['name']) + self.assertTrue('kid' in json) + self.assertTrue('public_key' in json) + self.assertTrue('private_key' in json) + + # Verify the private key is a valid PEM. + serialization.load_pem_private_key(json['private_key'].encode('utf-8'), None, default_backend()) + + # Verify the key. + kid = json['kid'] + + json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=kid)) + self.assertEquals('mynewkey', json['name']) + self.assertEquals('coolservice', json['service']) + self.assertEquals('baz', json['metadata']['foo']) + self.assertEquals(kid, json['kid']) + + self.assertIsNotNone(json['approval']) + self.assertEquals('ServiceKeyApprovalType.SUPERUSER', json['approval']['approval_type']) + self.assertEquals(ADMIN_ACCESS_USER, json['approval']['approver']['username']) + self.assertEquals('whazzup!?', json['approval']['notes']) + + # Ensure that there are logs for the creation and auto-approval. + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_create') + self.assertEquals(existing_log_count + 1, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) + + kind = LogEntryKind.get(LogEntryKind.name == 'service_key_approve') + self.assertEquals(existing_log_count + 1, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) + + class TestSuperUserManagement(ApiTestCase): def test_get_user(self): self.login(ADMIN_ACCESS_USER)