Unverified Commit 45860421 authored by Peter Stanko's avatar Peter Stanko
Browse files

Added clients endpoint for secrets management and tests for this endpoint

parent aab29ca1
...@@ -37,7 +37,7 @@ def register_namespaces(app: Flask): # pylint: disable=cyclic-import ...@@ -37,7 +37,7 @@ def register_namespaces(app: Flask): # pylint: disable=cyclic-import
from portal.rest.workers import workers_namespace from portal.rest.workers import workers_namespace
from portal.rest.users import users_namespace from portal.rest.users import users_namespace
from portal.rest.management import management_namespace from portal.rest.management import management_namespace
from portal.rest.client import client_namespace from portal.rest.client import client_namespace, clients_namespace
rest_api.add_namespace(auth_namespace) rest_api.add_namespace(auth_namespace)
rest_api.add_namespace(courses_namespace) rest_api.add_namespace(courses_namespace)
rest_api.add_namespace(roles_namespace) rest_api.add_namespace(roles_namespace)
...@@ -48,6 +48,7 @@ def register_namespaces(app: Flask): # pylint: disable=cyclic-import ...@@ -48,6 +48,7 @@ def register_namespaces(app: Flask): # pylint: disable=cyclic-import
rest_api.add_namespace(users_namespace) rest_api.add_namespace(users_namespace)
rest_api.add_namespace(management_namespace) rest_api.add_namespace(management_namespace)
rest_api.add_namespace(client_namespace) rest_api.add_namespace(client_namespace)
rest_api.add_namespace(clients_namespace)
if app.config.get('GITLAB_URL'): if app.config.get('GITLAB_URL'):
from portal.rest.gitlab import oauth_namespace from portal.rest.gitlab import oauth_namespace
......
...@@ -3,10 +3,13 @@ from flask_restplus import Namespace, Resource ...@@ -3,10 +3,13 @@ from flask_restplus import Namespace, Resource
from portal import logger from portal import logger
from portal.database.models import ClientType from portal.database.models import ClientType
from portal.rest import rest_helpers
from portal.rest.schemas import SCHEMAS from portal.rest.schemas import SCHEMAS
from portal.service import permissions from portal.service import permissions, general
from portal.service.secrets import create_secret, delete_secret
client_namespace = Namespace('client') client_namespace = Namespace('client')
clients_namespace = Namespace('clients')
log = logger.get_logger(__name__) log = logger.get_logger(__name__)
...@@ -20,3 +23,62 @@ class ClientController(Resource): ...@@ -20,3 +23,62 @@ class ClientController(Resource):
client = perm_service.client_owner client = perm_service.client_owner
schema = SCHEMAS.user() if client.type == ClientType.USER else SCHEMAS.worker() schema = SCHEMAS.user() if client.type == ClientType.USER else SCHEMAS.worker()
return schema.dump(client)[0], 200 return schema.dump(client)[0], 200
@clients_namespace.route('/<string:cid>/secrets')
@clients_namespace.param('wid', "Client's id")
@clients_namespace.response(404, 'Client not found')
class ClientSecretsController(Resource):
@jwt_required
@clients_namespace.response(200, 'List client secrets')
def get(self, cid: str):
permissions.PermissionsService().require.sysadmin_or_self(cid)
client = general.find_client(cid)
return SCHEMAS.dump('secrets', client.secrets)
@jwt_required
# @workers_namespace.response(201, 'Created worker secret', model=secret_schema)
def post(self, cid: str):
permissions.PermissionsService().require.sysadmin_or_self(cid)
data = rest_helpers.parse_request_data(action='create', resource='secret')
client = general.find_client(cid)
new_secret, value = create_secret(client, **data)
return {'id': new_secret.id, 'value': value}, 201
@clients_namespace.route('/<string:cid>/secrets/<string:sid>')
@clients_namespace.param('wid', "Client's id")
@clients_namespace.param('sid', "Secret id")
@clients_namespace.response(404, 'Client not found')
class ClientSecretController(Resource):
@jwt_required
@clients_namespace.response(200, 'Client secret detail')
def get(self, cid: str, sid: str):
permissions.PermissionsService().require.sysadmin()
client = general.find_client(cid)
secret = general.find_secret(client, sid)
return SCHEMAS.dump('secret', secret)
@jwt_required
@clients_namespace.response(204, 'Client secret deleted')
def delete(self, cid: str, sid: str):
permissions.PermissionsService().require.sysadmin_or_self(cid)
worker = general.find_client(cid)
delete_secret(worker, sid)
return '', 204
@jwt_required
@clients_namespace.response(204, 'Client secret updated')
@clients_namespace.response(403, 'Not allowed to update client secret')
def put(self, cid: str, sid: str):
permissions.PermissionsService().require.sysadmin_or_self(cid)
data = rest_helpers.parse_request_data(action='update', resource='secret', partial=True)
client = general.find_client(cid)
secret = general.find_secret(client, sid)
general.update_entity(secret, data, ['name', 'expires_at'])
return '', 204
...@@ -12,7 +12,6 @@ from portal.rest import rest_helpers ...@@ -12,7 +12,6 @@ from portal.rest import rest_helpers
from portal.rest.schemas import SCHEMAS from portal.rest.schemas import SCHEMAS
from portal.service import auth, errors, general, permissions from portal.service import auth, errors, general, permissions
from portal.service.general import find_client_owner from portal.service.general import find_client_owner
from portal.service.secrets import create_secret, delete_secret
from portal.service.users import UserService from portal.service.users import UserService
users_namespace = Namespace('users') users_namespace = Namespace('users')
...@@ -207,62 +206,6 @@ class UserEffectivePermissions(Resource): ...@@ -207,62 +206,6 @@ class UserEffectivePermissions(Resource):
return perm return perm
@users_namespace.route('/<string:uid>/secrets')
@users_namespace.param('uid', "User's id")
@users_namespace.response(404, 'User not found')
class UserSecretsController(Resource):
@jwt_required
@users_namespace.response(200, 'List user secrets')
def get(self, uid: str):
user = general.find_user(uid)
permissions.PermissionsService().require.sysadmin_or_self(uid)
return SCHEMAS.dump('secrets', user.secrets)
@jwt_required
# @users_namespace.response(201, 'Created user secret', model=secret_schema)
def post(self, uid):
user = general.find_user(uid)
permissions.PermissionsService().require.sysadmin_or_self(uid)
data = rest_helpers.parse_request_data(action='create', resource='secret')
new_secret, value = create_secret(user, **data)
return {'id': new_secret.id, 'value': value}, 201
@users_namespace.route('/<string:uid>/secrets/<string:sid>')
@users_namespace.param('uid', "User's id")
@users_namespace.param('sid', "Secret id")
@users_namespace.response(404, 'User not found')
class UserSecretController(Resource):
@jwt_required
@users_namespace.response(200, 'User secret detail')
def get(self, uid: str, sid: str):
permissions.PermissionsService().require.sysadmin()
user = general.find_user(uid)
secret = general.find_secret(user, sid)
return SCHEMAS.dump('secret', secret)
@jwt_required
@users_namespace.response(204, 'User secret deleted')
def delete(self, uid: str, sid: str):
user = general.find_user(uid)
permissions.PermissionsService().require.sysadmin_or_self(uid)
delete_secret(user, sid)
return '', 204
@jwt_required
@users_namespace.response(204, 'User secret updated')
@users_namespace.response(403, 'Not allowed to update user secret')
def put(self, uid: str, sid: str):
permissions.PermissionsService().require.sysadmin_or_self(uid)
data = rest_helpers.parse_request_data(action='update', resource='secret', partial=True)
user = general.find_user(uid)
secret = general.find_secret(user, sid)
general.update_entity(secret, data, ['name', 'expires_at'])
return '', 204
# #
# Helper functions # Helper functions
# #
......
...@@ -5,7 +5,6 @@ from portal import logger ...@@ -5,7 +5,6 @@ from portal import logger
from portal.rest import rest_helpers from portal.rest import rest_helpers
from portal.rest.schemas import SCHEMAS from portal.rest.schemas import SCHEMAS
from portal.service import general, permissions from portal.service import general, permissions
from portal.service.secrets import create_secret, delete_secret
from portal.service.workers import WorkerService from portal.service.workers import WorkerService
workers_namespace = Namespace('workers') workers_namespace = Namespace('workers')
...@@ -64,61 +63,4 @@ class WorkerResource(Resource): ...@@ -64,61 +63,4 @@ class WorkerResource(Resource):
worker = general.find_worker(wid) worker = general.find_worker(wid)
WorkerService(worker=worker).update_worker(data) WorkerService(worker=worker).update_worker(data)
return '', 204 return '', 204
\ No newline at end of file
@workers_namespace.route('/<string:wid>/secrets')
@workers_namespace.param('wid', "Worker's id")
@workers_namespace.response(404, 'Worker not found')
class WorkerSecretsController(Resource):
@jwt_required
@workers_namespace.response(200, 'List worker secrets')
def get(self, wid: str):
permissions.PermissionsService().require.sysadmin()
worker = general.find_worker(wid)
return SCHEMAS.dump('secrets', worker.secrets)
@jwt_required
# @workers_namespace.response(201, 'Created worker secret', model=secret_schema)
def post(self, wid: str):
permissions.PermissionsService().require.sysadmin()
data = rest_helpers.parse_request_data(action='create', resource='secret')
worker = general.find_worker(wid)
new_secret, value = create_secret(worker, **data)
return {'id': new_secret.id, 'value': value}, 201
@workers_namespace.route('/<string:wid>/secrets/<string:sid>')
@workers_namespace.param('wid', "Worker's id")
@workers_namespace.param('sid', "Secret id")
@workers_namespace.response(404, 'Worker not found')
class WorkerSecretController(Resource):
@jwt_required
@workers_namespace.response(200, 'Worker secret detail')
def get(self, wid: str, sid: str):
permissions.PermissionsService().require.sysadmin()
worker = general.find_worker(wid)
secret = general.find_secret(worker, sid)
return SCHEMAS.dump('secret', secret)
@jwt_required
@workers_namespace.response(204, 'Worker secret deleted')
def delete(self, wid: str, sid: str):
permissions.PermissionsService().require.sysadmin()
worker = general.find_worker(wid)
delete_secret(worker, sid)
return '', 204
@jwt_required
@workers_namespace.response(204, 'Worker secret updated')
@workers_namespace.response(403, 'Not allowed to update worker secret')
def put(self, wid: str, sid: str):
permissions.PermissionsService().require.sysadmin()
data = rest_helpers.parse_request_data(action='update', resource='secret', partial=True)
worker = general.find_worker(wid)
secret = general.find_secret(worker, sid)
general.update_entity(secret, data, ['name', 'expires_at'])
return '', 204
import json
import pytest
from tests.rest import utils
@pytest.fixture
def student_credentials():
return json.dumps({
"type": "username_password",
"identifier": "student1",
"secret": "123456"
})
@pytest.fixture
def worker_credentials():
return json.dumps({
"type": "secret",
"identifier": "executor",
"secret": "executor_secret"
})
def test_client_detail_using_student_cred(client, student_credentials):
path = f'/client'
response = utils.make_request(
client, path, 'get', credentials=student_credentials)
print(f"Response: {response.data}")
assert response.status_code == 200
assert response.mimetype == 'application/json'
data = response.json
assert data['username'] == 'student1'
assert data['type'] == 'ClientType.USER'
def test_client_detail_using_executor_cred(client, worker_credentials):
path = f'/client'
response = utils.make_request(
client, path, 'get', credentials=worker_credentials)
print(f"Response: {response.data}")
assert response.status_code == 200
assert response.mimetype == 'application/json'
data = response.json
assert data['name'] == 'executor'
assert data['type'] == 'ClientType.WORKER'
import json
import pytest
from portal.database.models import Client, Secret
from tests.rest import utils
@pytest.fixture
def student_credentials():
return json.dumps({
"type": "username_password",
"identifier": "student1",
"secret": "123456"
})
@pytest.fixture
def worker_credentials():
return json.dumps({
"type": "secret",
"identifier": "executor",
"secret": "executor_secret"
})
def test_client_detail_using_student_cred(client, student_credentials):
path = f'/client'
response = utils.make_request(
client, path, 'get', credentials=student_credentials)
print(f"Response: {response.data}")
assert response.status_code == 200
assert response.mimetype == 'application/json'
data = response.json
assert data['username'] == 'student1'
assert data['type'] == 'ClientType.USER'
def test_client_detail_using_executor_cred(client, worker_credentials):
path = f'/client'
response = utils.make_request(
client, path, 'get', credentials=worker_credentials)
print(f"Response: {response.data}")
assert response.status_code == 200
assert response.mimetype == 'application/json'
data = response.json
assert data['name'] == 'executor'
assert data['type'] == 'ClientType.WORKER'
def test_create_secret(client):
instance = Client.query.filter_by(codename="executor").first()
request_dict = dict(name="new_secret")
response = utils.make_request(client, f'/clients/{instance.id}/secrets',
headers={"content-type": "application/json"},
data=json.dumps(request_dict),
method='post')
assert response.status_code == 201
assert response.mimetype == 'application/json'
parsed = response.json
assert parsed['value'] is not None
assert parsed['id'] is not None
secrets = Secret.query.filter_by(client=instance)
assert len(secrets.all()) == 2
assert secrets.filter_by(name='new_secret').first()
def test_list_secret(client):
instance = Client.query.filter_by(codename="executor").first()
response = utils.make_request(client, f'/clients/{instance.id}/secrets',
headers={"content-type": "application/json"},
method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
secrets = utils.extract_data(response)
assert len(secrets) == 1
def test_delete_secret(client):
worker = Client.query.filter_by(codename="executor").first()
secret = Secret.query.filter_by(client=worker).first()
response = utils.make_request(client, f'/clients/{worker.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
method='delete')
assert response.status_code == 204
assert Secret.query.filter_by(client=worker).count() == 0
def test_read_secret(client):
instance = Client.query.filter_by(codename="executor").first()
secret = Secret.query.filter_by(client=instance).first()
response = utils.make_request(client, f'/clients/{instance.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
response_secret = utils.extract_data(response)
assert response_secret['id'] == secret.id
assert response_secret['name'] == secret.name
assert Secret.query.filter_by(client=instance).count() == 1
def test_update_secret(client):
instance = Client.query.filter_by(codename="executor").first()
request_dict = dict(name="new_name")
secret = Secret.query.filter_by(client=instance).first()
response = utils.make_request(client, f'/clients/{instance.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
data=json.dumps(request_dict),
method='put')
assert response.status_code == 204
updated_secret = Secret.query.filter_by(client=instance).first()
assert updated_secret.id == secret.id
assert updated_secret.name == "new_name"
assert Secret.query.filter_by(client=instance).count() == 1
...@@ -332,83 +332,3 @@ def test_user_reviews(client): ...@@ -332,83 +332,3 @@ def test_user_reviews(client):
assert len(user_reviews) == len(reviews) assert len(user_reviews) == len(reviews)
# the test is incomplete - only print # the test is incomplete - only print
print(f"user_reviews: {user_reviews}") print(f"user_reviews: {user_reviews}")
def test_create_secret(client):
user = User.query.filter_by(username="teacher1").first()
request_dict = dict(
name="new_secret",
)
response = utils.make_request(client, f'/users/{user.id}/secrets',
headers={"content-type": "application/json"},
data=json.dumps(request_dict),
method='post')
assert response.status_code == 201
assert response.mimetype == 'application/json'
parsed = response.json
assert parsed['value'] is not None
assert parsed['id'] is not None
secrets = Secret.query.filter_by(client=user)
assert len(secrets.all()) == 2
assert secrets.filter_by(name='new_secret').first()
def test_list_secret(client):
user = User.query.filter_by(username="teacher1").first()
response = utils.make_request(client, f'/users/{user.id}/secrets',
headers={"content-type": "application/json"},
method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
secrets = utils.extract_data(response)
assert len(secrets) == 1
def test_delete_secret(client):
user = User.query.filter_by(username="teacher1").first()
secret = Secret.query.filter_by(client=user).first()
response = utils.make_request(client, f'/users/{user.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
method='delete')
assert response.status_code == 204
assert Secret.query.filter_by(client=user).count() == 0
def test_read_secret(client):
user = User.query.filter_by(username="teacher1").first()
secret = Secret.query.filter_by(client=user).first()
response = utils.make_request(client, f'/users/{user.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
response_secret = utils.extract_data(response)
assert response_secret['id'] == secret.id
assert response_secret['name'] == secret.name
assert Secret.query.filter_by(client=user).count() == 1
def test_update_secret(client):
user = User.query.filter_by(username="teacher1").first()
request_dict = dict(
name="new_name",
)
secret = Secret.query.filter_by(client=user).first()
response = utils.make_request(client, f'/users/{user.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
data=json.dumps(request_dict),
method='put')
assert response.status_code == 204
updated_secret = Secret.query.filter_by(client=user).first()
assert updated_secret.id == secret.id
assert updated_secret.name == "new_name"
assert Secret.query.filter_by(client=user).count() == 1
...@@ -77,85 +77,3 @@ def test_delete(client): ...@@ -77,85 +77,3 @@ def test_delete(client):
assert response.mimetype == 'application/json' assert response.mimetype == 'application/json'
assert not Worker.query.filter_by(name="executor").first() assert not Worker.query.filter_by(name="executor").first()
def test_create_secret(client):
worker = Worker.query.filter_by(name="executor").first()
request_dict = dict(
name="new_secret",
)
response = utils.make_request(client, f'/workers/{worker.id}/secrets',
headers={"content-type": "application/json"},
data=json.dumps(request_dict),
method='post')
assert response.status_code == 201
assert response.mimetype == 'application/json'
parsed = response.json
assert parsed['value'] is not None
assert parsed['id'] is not None
secrets = Secret.query.filter_by(client=worker)
assert len(secrets.all()) == 2
assert secrets.filter_by(name='new_secret').first()
def test_list_secret(client):
worker = Worker.query.filter_by(name="executor").first()
response = utils.make_request(client, f'/workers/{worker.id}/secrets',
headers={"content-type": "application/json"},
method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
secrets = utils.extract_data(response)
assert len(secrets) == 1
def test_delete_secret(client):
worker = Worker.query.filter_by(name="executor").first()
secret = Secret.query.filter_by(client=worker).first()
response = utils.make_request(client, f'/workers/{worker.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
method='delete')
assert response.status_code == 204
assert Secret.query.filter_by(client=worker).count() == 0
def test_read_secret(client):
worker = Worker.query.filter_by(name="executor").first()
secret = Secret.query.filter_by(client=worker).first()
response = utils.make_request(client, f'/workers/{worker.id}/secrets/{secret.id}',
headers={"content-type": "application/json"},
method='get')
assert response.status_code == 200
assert response.mimetype == 'application/json'
response_secret = utils.extract_data(response)
assert response_secret['id'] == secret.id
assert response_secret['name'] == secret.name