Unverified Commit 370b1540 authored by Peter Stanko's avatar Peter Stanko
Browse files

Clients refactor - username for user and name for worker are just proxy for codename

parent 964d92ba
......@@ -79,6 +79,7 @@ class Client(db.Model):
id = db.Column(db.String(length=36), default=lambda: str(
uuid.uuid4()), primary_key=True)
type = db.Column(db.Enum(ClientType, name='ClientType'), nullable=False)
codename = db.Column(db.String(30), unique=True, nullable=False)
secrets = db.relationship('Secret', back_populates='client', uselist=True,
cascade="all, delete-orphan", passive_deletes=True)
......@@ -196,7 +197,6 @@ class User(EntityBase, Client):
uuid.uuid4()), primary_key=True)
uco = db.Column(db.Integer)
email = db.Column(db.String(50), unique=True, nullable=False)
username = db.Column(db.String(30), unique=True, nullable=False)
name = db.Column(db.String(50))
is_admin = db.Column(db.Boolean, default=False)
# optional - after password change
......@@ -210,6 +210,14 @@ class User(EntityBase, Client):
'polymorphic_identity': ClientType.USER,
}
@hybrid_property
def username(self):
return self.codename
@username.setter
def username(self, value):
self.codename = value
def is_self(self, eid):
return super().is_self(eid) or self.username == eid
......@@ -860,9 +868,8 @@ class Submission(db.Model, EntityBase):
uuid.uuid4()), primary_key=True)
scheduled_for = db.Column(db.TIMESTAMP(timezone=True))
parameters = db.Column(JSONEncodedDict(), nullable=True)
state = db.Column(
db.Enum(SubmissionState, name='SubmissionState'), nullable=False)
note = db.Column(db.Text)
state = db.Column(db.Enum(SubmissionState, name='SubmissionState'), nullable=False)
note = db.Column(JSONEncodedDict())
source_hash = db.Column(db.String(64))
user_id = db.Column(db.String(36), db.ForeignKey(
'user.id', ondelete='cascade'), nullable=False)
......@@ -1002,7 +1009,6 @@ class Worker(EntityBase, Client):
__tablename__ = 'worker'
id = db.Column(db.String(length=36), db.ForeignKey('client.id'), default=lambda: str(
uuid.uuid4()), primary_key=True)
name = db.Column(db.String(30), nullable=False, unique=True)
url = db.Column(db.String(250), nullable=False)
tags = db.Column(db.Text()) # set by a Worker at init
portal_secret = db.Column(db.String(64)) # set by a Worker at init
......@@ -1012,6 +1018,14 @@ class Worker(EntityBase, Client):
'polymorphic_identity': ClientType.WORKER,
}
@hybrid_property
def name(self):
return self.codename
@name.setter
def name(self, value):
self.codename = value
def is_self(self, eid):
return super().is_self(eid) or self.name == eid
......
......@@ -51,8 +51,9 @@ class NestedCollection:
'Group': ('id', 'codename', 'course.id'),
'Project': ('id', 'codename', 'course.id'),
'Course': ('id', 'codename'),
'User': ('id', 'username', 'uco'),
'Client': ('id', 'type', 'name'),
'User': ('id', 'username', 'uco', 'codename'),
'Worker': ('id', 'codename', 'name'),
'Client': ('id', 'type', 'name', 'codename'),
'Component': ('id', 'name', 'type'),
'Submission': ('id', 'state', 'user.id', 'user.username',
'project.id', 'project.codename',
......@@ -92,6 +93,7 @@ class UserSchema(BaseSchema, Schema):
email = fields.Email(required=True)
username = fields.Str(required=True)
name = fields.Str()
codename = fields.Str()
type = fields.Str()
is_admin = fields.Bool(default=False, missing=False)
submissions = NESTED['submissions']
......@@ -282,6 +284,7 @@ class WorkerSchema(BaseSchema, Schema):
"""Component Schema
"""
name = fields.Str()
codename = fields.Str(allow_none=True)
type = fields.Str()
url = fields.Str(allow_none=True)
tags = fields.Str(allow_none=True)
......
......@@ -7,9 +7,7 @@ from flask_jwt_extended import get_jwt_identity
from portal import gitlab_factory
from portal.database.models import Client
from portal.service.errors import IncorrectCredentialsError, InvalidGitlabAccessTokenError, \
PortalAPIError, UnauthorizedError
from portal.service.general import find_user, find_worker
from portal.service import general, errors
log = logging.getLogger(__name__)
......@@ -24,13 +22,13 @@ def login_gitlab(identifier: str, secret: str) -> Client:
Returns(User): the authenticated user
"""
if secret is None:
raise PortalAPIError(400, 'No gitlab access token found.')
raise errors.PortalAPIError(400, 'No gitlab access token found.')
validate_gitlab_token(secret, username=identifier)
user = find_user(identifier, throws=False)
user = general.find_user(identifier, throws=False)
if user is None:
raise InvalidGitlabAccessTokenError()
raise errors.InvalidGitlabAccessTokenError()
return user
......@@ -43,14 +41,14 @@ def login_username_password(identifier: str, secret: str) -> Client:
Returns(Client): the authenticated user
"""
user = find_user(identifier, throws=False)
user = general.find_user(identifier, throws=False)
if user is None or secret is None:
raise IncorrectCredentialsError()
raise errors.IncorrectCredentialsError()
if user.verify_password(password=secret):
return user
raise IncorrectCredentialsError()
raise errors.IncorrectCredentialsError()
def login_secret(identifier: str, secret: str) -> Client:
......@@ -66,7 +64,7 @@ def login_secret(identifier: str, secret: str) -> Client:
client = __find_client_helper(identifier)
if client.verify_secret(secret):
return client
raise UnauthorizedError(f"[LOGIN] Invalid secret.")
raise errors.UnauthorizedError(f"[LOGIN] Invalid secret.")
def validate_gitlab_token(token: str, username: str, throws: bool = True):
......@@ -82,7 +80,7 @@ def validate_gitlab_token(token: str, username: str, throws: bool = True):
user = client.user
if user.username != username:
if throws:
raise InvalidGitlabAccessTokenError()
raise errors.InvalidGitlabAccessTokenError()
return False
return True
......@@ -93,10 +91,8 @@ def find_client() -> Client:
def __find_client_helper(identifier: str) -> Client:
log.debug(f"[LOGIN] Finding permissions using identifier: {identifier}")
client = find_user(identifier, throws=False)
log.debug(f"[LOGIN] Finding client using identifier: {identifier}")
client = general.find_client(identifier, throws=False)
if not client:
client = find_worker(identifier, throws=False)
if not client:
raise UnauthorizedError(f"[LOGIN] Unknown permissions identifier {identifier}.")
raise errors.UnauthorizedError(f"[LOGIN] Unknown client identifier {identifier}.")
return client
......@@ -108,7 +108,7 @@ class UnauthorizedError(PortalAPIError):
class ForbiddenError(PortalAPIError):
# could use a resource identification (like 404)
def __init__(self, client=None, note=None):
user_message = f"Forbidden for {permissions.type}: {permissions.id}!" if client else \
user_message = f"Forbidden for {client.type}: {client.id} ({client.codename})!" if client else \
'Forbidden action.'
message = dict(uid=client.id, message=user_message)
if note:
......
......@@ -78,8 +78,7 @@ def find_resource(identifier: str, resource: str, query, throws: bool = True):
raise ResourceNotFoundError(resource=resource, res_id=identifier)
else:
return None
log.debug(
f"[FIND] Found {resource.capitalize()} for identifier [{identifier}]: {instance}")
log.debug(f"[FIND] Found {resource.capitalize()} for identifier [{identifier}]: {instance}")
return instance
......@@ -252,17 +251,12 @@ def find_client(identifier: str, throws=False, client_type=None) -> Client:
Returns(Client): User entity instance
"""
query = Client.query.filter(Client.id == identifier)
if client_type == 'user':
query = User.query.filter((User.id == identifier) | (User.username == identifier))
elif client_type == 'worker':
query = Worker.query.filter((Worker.id == identifier) | (Worker.name == identifier))
register = dict(user=User, client=Client, worker=Worker)
klass = register.get(client_type) or Client
query = klass.query.filter((klass.id == identifier) | (klass.codename == identifier))
return find_resource(
resource='permissions',
resource=client_type or 'client',
identifier=identifier,
query=query,
throws=throws
......@@ -298,4 +292,4 @@ def find_client_owner(client: Client) -> Union[User, Worker]:
return find_user(client.id)
if client.type == ClientType.WORKER:
return find_worker(client.id)
raise UnauthorizedError(f"[LOGIN] Unknown permissions type {permissions.type}.")
raise UnauthorizedError(f"[LOGIN] Unknown client type {client.type}.")
......@@ -5,7 +5,7 @@ Permissions service
import logging
from typing import Union
from portal.database.models import ClientType, Course, Role, User, Worker
from portal.database.models import ClientType, Course, Role, Submission, User, Worker
from portal.service import auth, general
from portal.service.errors import ForbiddenError
......@@ -35,9 +35,9 @@ class PermissionServiceCheck:
return False
def permissions(self, permissions) -> bool:
log.debug(
f"[PERM] Client {self.service.client.id} in course "
f"{self.service.course.codename}: {permissions}")
client = self.service.client
log.debug(f"[PERM] Client {client.id} ({client.codename}) in course "
f"\"{self.service.course.codename}\": {permissions}")
if self.sysadmin():
return True
......@@ -48,28 +48,53 @@ class PermissionServiceCheck:
res = any(_resolve_permissions(conjunction, effective_permissions) for conjunction in
permissions)
log.debug(f"[PERM] {permissions} for {self.service.client.id}: {res}")
log.debug(f"[PERM] {permissions} for {client.id} ({client.codename}): {res}")
return res
def any_check(self, *params) -> bool:
return any(params)
def view_course_full(self) -> bool:
return self.permissions(['view_course_full'])
def view_course_limited(self) -> bool:
return self.permissions(['view_course_limited'])
def view_course_any(self) -> bool:
return self.permissions(['view_course_limited', 'view_course_full'])
def read_submissions_all(self) -> bool:
return self.permissions(['read_submissions_all'])
def read_submissions_group(self, submission: Submission) -> bool:
return self.read_submissions_all() or \
self.service.submission_access_group(submission, ['read_submissions_groups'])
def read_submissions_own(self, submission: Submission) -> bool:
return self.read_submissions_all() or \
self.permissions(['read_submissions_own']) and \
submission.user == self.service.client_owner
class PermissionServiceRequire:
def __init__(self, service: 'PermissionsService'):
self.service = service
@property
def _check(self) -> PermissionServiceCheck:
return self.service.check
def sysadmin_or_self(self, eid):
self.any_check(self.service.check.sysadmin_or_self(eid))
self.any_check(self._check.sysadmin_or_self(eid))
def sysadmin(self):
self.any_check(self.service.check.sysadmin())
self.any_check(self._check.sysadmin())
def permissions(self, permissions):
self.any_check(self.service.check.permissions(permissions))
self.any_check(self._check.permissions(permissions))
def any_check(self, *checks):
if not self.service.check.any_check(*checks):
if not self._check.any_check(*checks):
raise ForbiddenError(self.service.client)
def update_course(self):
......@@ -85,45 +110,42 @@ class PermissionServiceRequire:
self.permissions(['update_course', 'write_groups'])
def view_course(self):
self.permissions(['view_course_full', 'view_course_limited'])
self._check.view_course_any()
def belongs_to_group(self, group):
checks = [
self.service.check.permissions(['view_course_full']),
(self.service.check.permissions(['view_course_limited'])
self._check.view_course_full(),
(self._check.view_course_limited()
and self.service.client_owner in group.users)
]
self.any_check(*checks)
def belongs_to_role(self, role: Role):
checks = [
self.service.check.permissions(['view_course_full']),
(self.service.check.permissions(['view_course_limited'])
self._check.view_course_full(),
(self._check.view_course_limited()
and self.service.client_owner in role.clients)
]
self.any_check(*checks)
def read_submission(self, submission):
checks = [
self.service.check.permissions(['read_submissions_all']),
self.service.submission_access_group(submission, ['read_submissions_groups']),
(self.service.check.permissions(['read_submissions_own']) and
submission.user == self.service.client_owner)
self._check.read_submissions_group(submission),
self._check.read_submissions_own(submission=submission)
]
self.any_check(*checks)
def read_submission_group(self, submission):
checks = [
self.service.check.permissions(['read_submissions_all']),
self.service.submission_access_group(submission, ['read_submissions_groups'])
self._check.read_submissions_group(submission=submission)
]
self.any_check(*checks)
def write_review_for_submission(self, submission):
checks = [
self.service.check.permissions(['write_reviews_all']),
self._check.permissions(['write_reviews_all']),
self.service.submission_access_group(submission, ['write_reviews_group']),
(self.service.check.permissions(['write_reviews_own']) and
(self._check.permissions(['write_reviews_own']) and
submission.user == self.service.client_owner)
]
self.any_check(*checks)
......@@ -133,7 +155,7 @@ class PermissionServiceRequire:
self.permissions(['create_submissions_other'])
user_service = PermissionsService(course=self.service.course, client=user)
user_service.require.permissions('create_submissions')
user_service.require.permissions(['create_submissions'])
def course_access_token(self):
self.permissions(['handle_notes_access_token'])
......@@ -207,7 +229,7 @@ class PermissionsService:
if not key.startswith("_") and key not in FILTER_PERMISSION_ATTRS:
result[key] = result.get(key) or value
log.debug(
f"[PERM] Effective permissions: {self.permissions.id} "
f"[PERM] Effective permissions: {self.client.id} "
f"in course {course.codename}: {result}")
return result
......
......@@ -138,7 +138,7 @@ class RoleService:
return clients
def add_client(self, client: Client):
"""Adds single permissions to the role
"""Adds single client to the role
Args:
client(Client): Client instance
Returns:
......@@ -146,14 +146,14 @@ class RoleService:
if client not in self.role.clients:
self.role.clients.append(client)
write_entity(self.role)
log.info(f"[ADD] Client {permissions.id} to role "
log.info(f"[ADD] Client {client.id} to role "
f"{self.role.id} in course {self.role.course.id}.")
else:
log.info(f"[ADD] Client {permissions.id} is already "
log.info(f"[ADD] Client {client.id} is already "
f"in role {self.role.id} in course {self.role.course.id}: no change.")
def remove_client(self, client: Client) -> Role:
"""Removes single permissions from the role
"""Removes single client from the role
Args:
role(Role): Role instance
client(Client): Client instance
......@@ -164,10 +164,10 @@ class RoleService:
self.role.clients.remove(client)
write_entity(self.role)
except ValueError:
raise PortalAPIError(400, message=f"Could not remove permissions {permissions.id} "
raise PortalAPIError(400, message=f"Could not remove client {client.id} "
f"from role {self.role.id} in course {course.id}: "
f"role does not contain permissions.")
log.info(f"[REMOVE] Client {permissions.id} from role "
f"role does not contain client.")
log.info(f"[REMOVE] Client {client.id} from role "
f"{self.role.id} in course {course.id}.")
return self.role
......
......@@ -140,12 +140,12 @@ class SubmissionsService(object):
new_state = data['state']
if isinstance(client, User) and new_state != SubmissionState.CANCELLED:
raise errors.ForbiddenError(client,
note=f"User {permissions.id} cannot update "
note=f"User {client.id} cannot update "
f"state to other than CANCELLED.")
self.submission.change_state(new_state)
write_entity(self.submission)
log.info(f"[UPDATE] Submission state {self.submission.id} "
f"by {permissions.id} to {self.submission.state}")
f"by {client.id} to {self.submission.state}")
return self.submission
def cancel_submission(self):
......
......@@ -24,7 +24,7 @@ def worker_credentials():
def test_client_detail_using_student_cred(client, student_credentials):
path = f'/permissions'
path = f'/client'
response = utils.make_request(
client, path, 'get', credentials=student_credentials)
print(f"Response: {response.data}")
......@@ -36,7 +36,7 @@ def test_client_detail_using_student_cred(client, student_credentials):
def test_client_detail_using_executor_cred(client, worker_credentials):
path = f'/permissions'
path = f'/client'
response = utils.make_request(
client, path, 'get', credentials=worker_credentials)
print(f"Response: {response.data}")
......
import json
from datetime import timedelta
from flask_jwt_extended import create_access_token
from portal.database.models import Course, Project, User
......@@ -31,7 +32,7 @@ def test_create(client):
"name": "new_project",
'codename': 'new-project',
'description': 'ultimate project blah'
}
}
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{cpp.codename}/projects", method='post',
data=request_json,
......@@ -66,7 +67,7 @@ def test_update(client):
p_name = p.name
request_dict = dict(
name="new project name",
)
)
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{cpp.codename}/projects/{p.name}",
data=request_json, method='put',
......@@ -118,7 +119,7 @@ def test_config_update(client):
test_files_source="new source",
pre_submit_script="a python script",
submissions_allowed_to=new_time
)
)
request_json = json.dumps(request_dict, cls=utils.DateTimeEncoder)
response = utils.make_request(client, f"/courses/{cpp.codename}/projects/{p.name}/config",
......@@ -183,9 +184,9 @@ def test_create_submission(client):
"url": "https://gitlab.fi.muni.cz/xkompis/test-hello-world.git",
"branch": "master",
"checkout": "master"
}
}
}
}
request_json = json.dumps(request_dict, cls=utils.DateTimeEncoder)
path = f"/courses/{cpp.codename}/projects/{p.name}/submissions"
......@@ -203,3 +204,36 @@ def test_create_submission(client):
assert len(p_updated.submissions) == p_submissions + 1
utils.assert_submission_in(p_updated.submissions, new_submission)
utils.assert_submission_in(user_updated.submissions, new_submission)
def test_create_submission_as_different_user(client):
cpp = Course.query.filter_by(codename="testcourse1").first()
p = cpp.projects[0]
p_submissions = len(p.submissions)
request_dict = {
"project_params": "data for Kontr",
"file_params": {
"source": {
"type": "git",
"url": "https://gitlab.fi.muni.cz/xkompis/test-hello-world.git",
"branch": "master",
"checkout": "master"
}
}
}
request_json = json.dumps(request_dict, cls=utils.DateTimeEncoder)
path = f"/courses/{cpp.codename}/projects/{p.name}/submissions?user=student1"
response = utils.make_request(client, path, data=request_json, method='post')
assert response.status_code == 201
assert response.mimetype == 'application/json'
new_submission = utils.extract_data(response)
p_updated = Project.query.filter(Project.course_id == cpp.id).filter_by(name=p.name).first()
user_updated = User.query.filter_by(username="student1").first()
assert len(p_updated.submissions) == p_submissions + 1
utils.assert_submission_in(p_updated.submissions, new_submission)
utils.assert_submission_in(user_updated.submissions, new_submission)
......@@ -27,7 +27,7 @@ def test_create(client):
"name": "new role",
'codename': 'new-role',
"description": "new role desc"
}
}
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{cpp.codename}/roles", data=request_json,
headers={"content-type": "application/json"}, method='post')
......@@ -63,7 +63,7 @@ def test_update(client):
request_dict = dict(
name="new role name",
description="new role desc",
)
)
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{cpp.codename}/roles/{r.name}",
data=request_json,
......@@ -118,7 +118,7 @@ def test_users_update_add(client):
request_dict = dict(
add=[user.id]
)
)
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{c.codename}/roles/{r.name}/clients",
data=request_json,
......@@ -143,7 +143,7 @@ def test_users_update_add_duplicate(client):
request_dict = dict(
add=[user.id]
)
)
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{c.codename}/roles/{r.name}/clients",
data=request_json,
......@@ -168,7 +168,7 @@ def test_users_update_remove(client):
request_dict = dict(
remove=[user.id]
)
)
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{c.codename}/roles/{r.name}/clients",
data=request_json,
......@@ -193,7 +193,7 @@ def test_users_update_remove_user_not_in(client):
request_dict = dict(
remove=[user.id]
)
)
request_json = json.dumps(request_dict)
response = utils.make_request(client, f"/courses/{c.codename}/roles/{r.name}/clients",
data=request_json,
......@@ -305,7 +305,7 @@ def test_permissions_update(client):
view_course_full=True,
write_roles=True,
read_submissions_all=True
)
)
request_json = json.dumps(request_dict, cls=utils.DateTimeEncoder)
response = utils.make_request(client, f"/courses/{cpp.codename}/roles/{r.name}/permissions",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment