Unverified Commit 964d92ba authored by Peter Stanko's avatar Peter Stanko
Browse files

New permissions for creating new submissions by other user

parent ef048f2e
......@@ -35,6 +35,8 @@ PERM_TEACHER = dict(
write_groups=True,
write_projects=True,
archive_projects=True,
create_submissions_other=True,
create_submissions=True,
resubmit_submissions=True,
evaluate_submissions=True,
write_reviews_all=True
......
......@@ -71,9 +71,9 @@ class Client(db.Model):
id: UUID string
name: custom name for the secret
type: client type (worker or user)
secrets: a list of secrets for this client
roles: roles associated with this client
owner_id: reference to the enclosing entity of the client
- secrets: a list of secrets for this client
- roles: roles associated with this client
- owner_id: reference to the enclosing entity of the client
"""
__tablename__ = 'client'
id = db.Column(db.String(length=36), default=lambda: str(
......@@ -744,6 +744,7 @@ class RolePermissions(db.Model, EntityBase):
archive_projects = db.Column(db.Boolean, default=False, nullable=False)
create_submissions = db.Column(db.Boolean, default=False, nullable=False)
create_submissions_other = db.Column(db.Boolean, default=False, nullable=False)
resubmit_submissions = db.Column(db.Boolean, default=False, nullable=False)
evaluate_submissions = db.Column(db.Boolean, default=False, nullable=False)
......
......@@ -53,10 +53,10 @@ class CourseResource(Resource):
course = find_course(cid)
# authorization
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.client(['view_course_full']):
if perm_service.check.permissions(['view_course_full']):
return course_schema.dump(course)
elif perm_service.check.client(['view_course_limited']):
elif perm_service.check.permissions(['view_course_limited']):
dump = course_schema.dump(course)
filtered_course = filter_course_dump(course, dump.data, client)
return filtered_course
......@@ -78,7 +78,7 @@ class CourseResource(Resource):
def put(self, cid: str):
course = find_course(cid)
# authorization
permissions.PermissionsService(course=course).require.client(['update_course'])
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
schema=course_schema, action='update', resource='course', partial=True
......@@ -98,7 +98,7 @@ class CourseNotesToken(Resource):
def get(self, cid):
course = find_course(cid)
# authorization
permissions.PermissionsService(course=course).require.client(['handle_notes_access_token'])
permissions.PermissionsService(course=course).require.course_access_token()
return course.notes_access_token
@jwt_required
......@@ -108,7 +108,7 @@ class CourseNotesToken(Resource):
def put(self, cid):
course = find_course(cid)
# authorization
permissions.PermissionsService(course=course).require.client(['handle_notes_access_token'])
permissions.PermissionsService(course=course).require.course_access_token()
json_data = rest_helpers.require_data(
action='update_notes_token', resource='course')
......@@ -128,7 +128,7 @@ class CourseImport(Resource):
def put(self, cid: str):
course = find_course(cid)
# authorization
permissions.PermissionsService(course=course).require.client(['update_course'])
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
course_import_schema, action='import', resource='course'
......@@ -154,7 +154,7 @@ class CourseUsers(Resource):
@courses_namespace.response(403, 'Not allowed to see users in the course')
def get(self, cid):
course = find_course(cid)
permissions.PermissionsService(course=course).require.client(['view_course_full'])
permissions.PermissionsService(course=course).require.permissions(['view_course_full'])
group_ids = request.args.getlist('group')
role_ids = request.args.getlist('role')
users = CourseService(course=course).get_users_filtered(group_ids, role_ids)
......
......@@ -33,9 +33,7 @@ class GroupsList(Resource):
# authorization
permissions.PermissionsService(course=course).require.update_course()
data = rest_helpers.parse_request_data(
group_schema, action='create', resource='group'
)
data = rest_helpers.parse_request_data(group_schema, action='create', resource='group')
new_group = GroupService().create_group(course, **data)
return group_schema.dump(new_group)[0], 201
......
......@@ -5,7 +5,7 @@ from flask_restplus import Namespace, Resource, fields
from portal import logger
from portal.database.models import Client
from portal.service.auth import login_gitlab, login_username_password, login_secret
from portal.service.auth import login_gitlab, login_secret, login_username_password
from portal.service.errors import PortalAPIError, UnauthorizedError
log = logger.get_logger(__name__)
......@@ -50,8 +50,8 @@ class Refresh(Resource):
@auth_namespace.marshal_with(refresh_schema)
@auth_namespace.response(401, 'Client is not authorized')
def post(self):
client = authorized_client()
return dict(access_token=create_access_token(identity=client))
client_id = authorized_client()
return dict(access_token=create_access_token(identity=client_id))
@auth_namespace.route('/logout')
......@@ -60,11 +60,11 @@ class Logout(Resource):
@auth_namespace.marshal_with(logout_schema)
@auth_namespace.response(401, 'Client is not authorized')
def post(self):
authorized_client()
return dict(access_token=None, refresh_token=None)
client_id = authorized_client()
return dict(id=client_id, access_token=None, refresh_token=None)
def authorized_client():
def authorized_client() -> str:
client = get_jwt_identity()
if not client:
raise UnauthorizedError()
......
......@@ -155,7 +155,7 @@ class ProjectSubmissions(Resource):
course = general.find_course(cid)
# authorization
perm = ['read_submissions_all']
permissions.PermissionsService(course=course).require.client(perm)
permissions.PermissionsService(course=course).require.permissions(perm)
user_id = request.args.get('user')
project = general.find_project(course, pid)
......@@ -167,13 +167,14 @@ class ProjectSubmissions(Resource):
def post(self, cid: str, pid: str):
client = auth.find_client()
course = general.find_course(cid)
user = request.args.get('user')
user = general.find_user(user) if user is not None else client
# authorization
perm = ['create_submissions']
permissions.PermissionsService(course=course).require.client(perm)
permissions.PermissionsService(course=course).require.create_submission(user)
# check if a new submission can be created by this user in this project
project = general.find_project(course, pid)
_check_submission_create(client, project)
_check_submission_create(user, project)
data = rest_helpers.parse_request_data(
submission_create_schema, action='create', resource='submission'
......@@ -183,7 +184,7 @@ class ProjectSubmissions(Resource):
# data for Kontr processing
service = SubmissionsService()
new_submission = service.create_submission(
user=client,
user=user,
project=project,
submission_params=data
)
......@@ -216,9 +217,9 @@ def _check_submission_create(client, project):
def get_config_schema_based_on_permissions(course):
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.client(['view_course_full']):
if perm_service.check.permissions(['view_course_full']):
return config_schema
elif perm_service.check.client(['view_course_limited']):
elif perm_service.check.permissions(['view_course_limited']):
return config_schema_reduced
else:
raise ForbiddenError(perm_service.client)
......@@ -163,7 +163,7 @@ class RoleUsersList(Resource):
@roles_namespace.response(404, 'Client not found')
class RoleClient(Resource):
@jwt_required
@roles_namespace.response(204, 'Adds client to role')
@roles_namespace.response(204, 'Adds permissions to role')
def put(self, cid: str, rid: str, clid: str):
course = general.find_course(cid)
# authorization
......@@ -175,7 +175,7 @@ class RoleClient(Resource):
return '', 204
@jwt_required
@roles_namespace.response(204, 'Removes client from role')
@roles_namespace.response(204, 'Removes permissions from role')
def delete(self, cid: str, rid: str, clid: str):
course = general.find_course(cid)
permissions.PermissionsService(course=course).require.write_roles()
......
......@@ -224,6 +224,7 @@ class RolePermissionsSchema(BaseSchema, Schema):
archive_projects = fields.Bool()
create_submissions = fields.Bool()
create_submissions_other = fields.Bool()
resubmit_submissions = fields.Bool()
evaluate_submissions = fields.Bool()
......@@ -293,7 +294,7 @@ class SecretSchema(BaseSchema, Schema):
"""
name = fields.Str()
expires_at = fields.LocalDateTime(dump_only=True)
client = NESTED('client')
client = NESTED('permissions')
class CourseImportConfigSchema(Schema):
......
......@@ -167,7 +167,7 @@ class SubmissionResultFiles(Resource):
submission = general.find_submission(sid)
# authorization
course = submission.project.course
permissions.PermissionsService(course=course).require.client(['evaluate_submissions'])
permissions.PermissionsService(course=course).require.permissions(['evaluate_submissions'])
# todo: authorize worker
service = SubmissionsService(submission=submission)
task = service.upload_results_to_storage()
......@@ -185,7 +185,7 @@ class SubmissionResubmit(Resource):
# authorization
course = source_submission.project.course
perm_service = permissions.PermissionsService(course=course)
perm_service.require.client(permissions=['resubmit_submissions'])
perm_service.require.permissions(permissions=['resubmit_submissions'])
data = rest_helpers.parse_request_data(
submission_schema, action='resubmit', resource='submission'
......
......@@ -304,10 +304,10 @@ def get_submissions_based_on_permissions_for_course(client, course_id, project_i
perm_service = permissions.PermissionsService(course=course)
checks = [
perm_service.check.sysadmin(),
perm_service.check.client(['read_submissions_all']),
(perm_service.check.client(['read_submissions_groups'])
perm_service.check.permissions(['read_submissions_all']),
(perm_service.check.permissions(['read_submissions_groups'])
and find_groups_intersection(client_owner, user)),
(perm_service.check.client(['read_submissions_own'])
(perm_service.check.permissions(['read_submissions_own'])
and client_owner == user)
]
perm_service.require.any_check(client, *checks)
......
......@@ -70,7 +70,7 @@ def login_secret(identifier: str, secret: str) -> Client:
def validate_gitlab_token(token: str, username: str, throws: bool = True):
"""Validates gitlab access token using the gitlab client
"""Validates gitlab access token using the gitlab permissions
Args:
token(str): Gitlab access token
username(str): Username
......@@ -93,10 +93,10 @@ def find_client() -> Client:
def __find_client_helper(identifier: str) -> Client:
log.debug(f"[LOGIN] Finding client using identifier: {identifier}")
log.debug(f"[LOGIN] Finding permissions using identifier: {identifier}")
client = find_user(identifier, throws=False)
if not client:
client = find_worker(identifier, throws=False)
if not client:
raise UnauthorizedError(f"[LOGIN] Unknown client identifier {identifier}.")
raise UnauthorizedError(f"[LOGIN] Unknown permissions 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 {client.type}: {client.id}!" if client else \
user_message = f"Forbidden for {permissions.type}: {permissions.id}!" if client else \
'Forbidden action.'
message = dict(uid=client.id, message=user_message)
if note:
......
......@@ -262,7 +262,7 @@ def find_client(identifier: str, throws=False, client_type=None) -> Client:
query = Worker.query.filter((Worker.id == identifier) | (Worker.name == identifier))
return find_resource(
resource='client',
resource='permissions',
identifier=identifier,
query=query,
throws=throws
......@@ -298,4 +298,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 client type {client.type}.")
raise UnauthorizedError(f"[LOGIN] Unknown permissions type {permissions.type}.")
......@@ -182,15 +182,15 @@ class GroupService:
"""List all groups
Args:
course(Course): Course instance
client: Client instance
permissions: Client instance
Returns(list): List of all groups
"""
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.client(['view_course_full']):
if perm_service.check.permissions(['view_course_full']):
return course.groups
elif perm_service.check.client(['view_course_limited']):
elif perm_service.check.permissions(['view_course_limited']):
return filters.filter_groups_from_course(course=course, user=perm_service.client)
raise ForbiddenError(perm_service.client)
......
......@@ -34,7 +34,7 @@ class PermissionServiceCheck:
return self.service.client.is_admin
return False
def client(self, permissions) -> bool:
def permissions(self, permissions) -> bool:
log.debug(
f"[PERM] Client {self.service.client.id} in course "
f"{self.service.course.codename}: {permissions}")
......@@ -65,69 +65,79 @@ class PermissionServiceRequire:
def sysadmin(self):
self.any_check(self.service.check.sysadmin())
def client(self, permissions):
self.any_check(self.service.check.client(permissions))
def permissions(self, permissions):
self.any_check(self.service.check.permissions(permissions))
def any_check(self, *checks):
if not self.service.check.any_check(*checks):
raise ForbiddenError(self.service.client)
def update_course(self):
self.client(['update_course'])
self.permissions(['update_course'])
def write_projects(self):
self.client(['update_course', 'write_projects'])
self.permissions(['update_course', 'write_projects'])
def write_roles(self):
self.client(['update_course', 'write_roles'])
self.permissions(['update_course', 'write_roles'])
def write_groups(self):
self.client(['update_course', 'write_groups'])
self.permissions(['update_course', 'write_groups'])
def view_course(self):
self.client(['view_course_full', 'view_course_limited'])
self.permissions(['view_course_full', 'view_course_limited'])
def belongs_to_group(self, group):
checks = [
self.service.check.client(['view_course_full']),
(self.service.check.client(['view_course_limited'])
self.service.check.permissions(['view_course_full']),
(self.service.check.permissions(['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.client(['view_course_full']),
(self.service.check.client(['view_course_limited'])
self.service.check.permissions(['view_course_full']),
(self.service.check.permissions(['view_course_limited'])
and self.service.client_owner in role.clients)
]
self.any_check(*checks)
def read_submission(self, submission):
checks = [
self.service.check.client(['read_submissions_all']),
self.service.check.permissions(['read_submissions_all']),
self.service.submission_access_group(submission, ['read_submissions_groups']),
(self.service.check.client(['read_submissions_own']) and
(self.service.check.permissions(['read_submissions_own']) and
submission.user == self.service.client_owner)
]
self.any_check(*checks)
def read_submission_group(self, submission):
checks = [
self.service.check.client(['read_submissions_all']),
self.service.check.permissions(['read_submissions_all']),
self.service.submission_access_group(submission, ['read_submissions_groups'])
]
self.any_check(*checks)
def write_review_for_submission(self, submission):
checks = [
self.service.check.client(['write_reviews_all']),
self.service.check.permissions(['write_reviews_all']),
self.service.submission_access_group(submission, ['write_reviews_group']),
(self.service.check.client(['write_reviews_own']) and
(self.service.check.permissions(['write_reviews_own']) and
submission.user == self.service.client_owner)
]
self.any_check(*checks)
def create_submission(self, user):
if user != self.service.client:
self.permissions(['create_submissions_other'])
user_service = PermissionsService(course=self.service.course, client=user)
user_service.require.permissions('create_submissions')
def course_access_token(self):
self.permissions(['handle_notes_access_token'])
class PermissionsService:
def __init__(self, client=None, course=None):
......@@ -157,14 +167,14 @@ class PermissionsService:
return self._course
def get_effective_permissions(self, course_id: str = None) -> dict:
"""Gets effective permissions for a client in course.
If no course is specified, returns the client's
"""Gets effective permissions for a permissions in course.
If no course is specified, returns the permissions's
effective permissions in all courses he is a part of.
Args:
course_id(str): Course ID
Returns(dict): Effective permissions for the client. Keys are course IDs, values
Returns(dict): Effective permissions for the permissions. Keys are course IDs, values
dictionaries of permissions with their values.
"""
course = general.find_course(course_id, throws=False)
......@@ -172,7 +182,7 @@ class PermissionsService:
return self.build_effective_permissions(*courses)
def build_effective_permissions(self, *courses) -> dict:
"""Builds effective permissions in a list of courses for the client
"""Builds effective permissions in a list of courses for the permissions
Args:
*courses: List of courses
......@@ -185,7 +195,7 @@ class PermissionsService:
return result
def effective_permissions_for_course(self, course: Course = None) -> dict:
"""Extracts effective permissions for a client in one course
"""Extracts effective permissions for a permissions in one course
Returns(dict): Effective permissions dictionary
"""
......@@ -197,12 +207,12 @@ 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.client.id} "
f"[PERM] Effective permissions: {self.permissions.id} "
f"in course {course.codename}: {result}")
return result
def submission_access_group(self, submission, perm):
if self.check.client(perm):
if self.check.permissions(perm):
group_intersection = [
group for group in self.client.groups if submission.user in group.users]
return any(
......
......@@ -145,15 +145,15 @@ class ProjectService:
"""List of all projects
Args:
course(Course): Course instance
client: Client instance
permissions: Client instance
Returns(list): list of projects
"""
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.client(['view_course_full']):
if perm_service.check.permissions(['view_course_full']):
return course.projects
elif perm_service.check.client(['view_course_limited']):
elif perm_service.check.permissions(['view_course_limited']):
return filters.filter_projects_from_course(course=course, user=perm_service.client)
raise ForbiddenError(perm_service.client)
......
......@@ -101,14 +101,14 @@ class RoleService:
"""Finds all clients based on their ids
Args:
ids(List[str]): List of client ids
ids(List[str]): List of permissions ids
Returns(List[Client]): List of clients
"""
return [find_client(i) for i in ids]
def update_clients_membership(self, data: dict) -> Role:
"""Updates client membership in the role
"""Updates permissions membership in the role
Args:
data(dict): Data provided to update role membership
......@@ -138,7 +138,7 @@ class RoleService:
return clients
def add_client(self, client: Client):
"""Adds single client to the role
"""Adds single permissions 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 {client.id} to role "
log.info(f"[ADD] Client {permissions.id} to role "
f"{self.role.id} in course {self.role.course.id}.")
else:
log.info(f"[ADD] Client {client.id} is already "
log.info(f"[ADD] Client {permissions.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 client from the role
"""Removes single permissions 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 client {client.id} "
raise PortalAPIError(400, message=f"Could not remove permissions {permissions.id} "
f"from role {self.role.id} in course {course.id}: "
f"role does not contain client.")
log.info(f"[REMOVE] Client {client.id} from role "
f"role does not contain permissions.")
log.info(f"[REMOVE] Client {permissions.id} from role "
f"{self.role.id} in course {course.id}.")
return self.role
......@@ -175,14 +175,14 @@ class RoleService:
"""List of all roles
Args:
course(Course): Course instance
client(Client): Client instance
permissions(Client): Client instance
Returns(list): List of roles
"""
perm_service = permissions.PermissionsService(course=course)
if perm_service.check.client(['view_course_full']):
if perm_service.check.permissions(['view_course_full']):
return course.roles
elif perm_service.check.client(['view_course_limited']):
elif perm_service.check.permissions(['view_course_limited']):
return filter_roles_from_course(course=course, client=perm_service.client)
raise ForbiddenError(perm_service.client)
......@@ -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 {client.id} cannot update "
note=f"User {permissions.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 {client.id} to {self.submission.state}")
f"by {permissions.id} to {self.submission.state}")
return self.submission
def cancel_submission(self):
......
......@@ -110,7 +110,7 @@ class UserService:
return self.user
def update_password(self, data: dict):
log.info(f"[UPDATE] User password: {self.user.id} by {self.client.id}")
log.info(f"[UPDATE] User password: {self.user.id} by {self.permissions.id}")
self.__require_param(data, 'new_password')
if self.client == self.user and not self.user.is_admin:
self.__check_old_password(data)
......@@ -127,7 +127,7 @@ class UserService:
raise IncorrectCredentialsError()