Commit f1ce5ea4 authored by Barbora Kompišová's avatar Barbora Kompišová
Browse files

Authorization + login&tests refactor

parent 0052e7df
import enum
class SubmissionState(enum.Enum):
CREATED = 1
READY = 2
QUEUED = 3
IN_PROGRESS = 4
FINISHED = 5
CANCELLED = 6
ABORTED = 7
ARCHIVED = 8
\ No newline at end of file
......@@ -6,6 +6,7 @@ from sqlalchemy import event
from sqlalchemy.ext.hybrid import hybrid_property
from werkzeug.security import generate_password_hash, check_password_hash
from database import SubmissionState
from portal import db
from portal.database.mixins import EntityBase
from portal.database.exceptions import PortalDbError
......@@ -136,6 +137,8 @@ class Project(db.Model, EntityBase):
)
def state(self, timestamp=datetime.datetime.now()) -> ProjectState:
if not (self.config.submissions_allowed_from or self.config.submissions_allowed_to or self.config.archive_from):
return ProjectState.INACTIVE
if self.config.submissions_allowed_from <= timestamp < self.config.submissions_allowed_to:
return ProjectState.ACTIVE
elif timestamp >= self.config.archive_from:
......@@ -268,30 +271,38 @@ class RolePermissions(db.Model, EntityBase):
role = db.relationship("Role", back_populates="permissions", uselist=False)
# all default to False, explicit setting via role.set_permissions is required
viewCourseLimited = db.Column(db.Boolean, default=False, nullable=False)
viewCourseFull = db.Column(db.Boolean, default=False, nullable=False)
updateCourse = db.Column(db.Boolean, default=False, nullable=False)
setNotesAccessToken = db.Column(db.Boolean, default=False, nullable=False)
assignRoles = db.Column(db.Boolean, default=False, nullable=False)
writeRole = db.Column(db.Boolean, default=False, nullable=False)
readRole = db.Column(db.Boolean, default=False, nullable=False)
writeProject = db.Column(db.Boolean, default=False, nullable=False)
deleteProject = db.Column(db.Boolean, default=False, nullable=False)
archiveProject = db.Column(db.Boolean, default=False, nullable=False)
writeGroup = db.Column(db.Boolean, default=False, nullable=False)
readGroup = db.Column(db.Boolean, default=False, nullable=False)
deleteGroup = db.Column(db.Boolean, default=False, nullable=False)
readAllSubmissions = db.Column(db.Boolean, default=False, nullable=False)
readOwnSubmissions = db.Column(db.Boolean, default=False, nullable=False)
readGroupSubmissions = db.Column(db.Boolean, default=False, nullable=False) # OPTIONAL
viewAllSubmissionFiles = db.Column(db.Boolean, default=False, nullable=False)
createSubmission = db.Column(db.Boolean, default=False, nullable=False)
resubmitSubmission = db.Column(db.Boolean, default=False, nullable=False)
readAllReviews = db.Column(db.Boolean, default=False, nullable=False)
readOwnReviews = db.Column(db.Boolean, default=False, nullable=False)
writeOwnReview = db.Column(db.Boolean, default=False, nullable=False)
writeAllReview = db.Column(db.Boolean, default=False, nullable=False)
evaluateSubmission = db.Column(db.Boolean, default=False, nullable=False)
view_course_limited = db.Column(db.Boolean, default=False, nullable=False)
view_course_full = db.Column(db.Boolean, default=False, nullable=False)
update_course = db.Column(db.Boolean, default=False, nullable=False)
handle_notes_access_token = db.Column(db.Boolean, default=False, nullable=False)
assign_roles = db.Column(db.Boolean, default=False, nullable=False)
write_roles = db.Column(db.Boolean, default=False, nullable=False)
read_roles = db.Column(db.Boolean, default=False, nullable=False)
write_groups = db.Column(db.Boolean, default=False, nullable=False)
read_groups = db.Column(db.Boolean, default=False, nullable=False)
write_projects = db.Column(db.Boolean, default=False, nullable=False)
read_projects = db.Column(db.Boolean, default=False, nullable=False)
archive_projects = db.Column(db.Boolean, default=False, nullable=False)
create_submissions = 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)
read_submissions_all = db.Column(db.Boolean, default=False, nullable=False)
read_submissions_groups = db.Column(db.Boolean, default=False, nullable=False)
read_submissions_own = db.Column(db.Boolean, default=False, nullable=False)
read_all_submission_files = db.Column(db.Boolean, default=False, nullable=False)
read_reviews_all = db.Column(db.Boolean, default=False, nullable=False)
read_reviews_groups = db.Column(db.Boolean, default=False, nullable=False)
read_reviews_own = db.Column(db.Boolean, default=False, nullable=False)
write_reviews_all = db.Column(db.Boolean, default=False, nullable=False)
write_reviews_group = db.Column(db.Boolean, default=False, nullable=False)
write_reviews_own = db.Column(db.Boolean, default=False, nullable=False)
def __init__(self, role):
self.role = role
......@@ -326,17 +337,6 @@ class Group(db.Model, EntityBase):
return self.id == other.id
class SubmissionState(enum.Enum):
CREATED = 1
READY = 2
QUEUED = 3
IN_PROGRESS = 4
FINISHED = 5
CANCELLED = 6
ABORTED = 7
ARCHIVED = 8
class Submission(db.Model, EntityBase):
__tablename__ = 'submission'
id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
......@@ -418,7 +418,7 @@ class Component(db.Model, EntityBase):
__tablename__ = 'component'
id = db.Column(db.Text(length=36), default=lambda: str(uuid.uuid4()), primary_key=True)
name = db.Column(db.String(30), nullable=False, unique=True)
access_token = db.Column(db.String(100))
secret = db.Column(db.String(250))
ip_address = db.Column(db.String(50))
......
from marshmallow import Schema, fields, validates_schema, ValidationError
from marshmallow_enum import EnumField
from portal.database.models import SubmissionState
from portal.database.models import ProjectState
from database import SubmissionState
class UserSchema(Schema):
id = fields.Str(dump_only=True, required=True)
uco = fields.Int(required=True)
email = fields.Email(required=True)
username = fields.Str(required=True)
username = fields.Str()
name = fields.Str()
is_admin = fields.Bool(default=False)
is_admin = fields.Bool(default=False, missing=False)
submissions = fields.Nested('portal.rest.SubmissionSchema', only=('id', 'note', 'state'), many=True)
review_items = fields.Nested('portal.rest.ReviewItemSchema', only=('id',), many=True)
......@@ -54,12 +55,12 @@ class SubmissionCreateSchema(Schema):
class ProjectSchema(Schema):
# TODO: add state as enum
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
config = fields.Nested('portal.rest.ProjectConfigSchema', exclude=('id', '_submissions_allowed_from', '_submissions_allowed_to',
'_archive_from')) # TODO: check hybrid property behaviour
course = fields.Nested(CourseSchema, only=('id', 'name', 'codename')) # schema name in '' failed
config = fields.Nested('portal.rest.ProjectConfigSchema', exclude=('id', '_submissions_allowed_from',
'_submissions_allowed_to', '_archive_from'))
state = EnumField(ProjectState)
course = fields.Nested(CourseSchema, only=('id', 'name', 'codename'))
submissions = fields.Nested('portal.rest.SubmissionSchema', many=True, only=('id', 'note', 'state', 'user'))
......@@ -90,30 +91,38 @@ class RoleSchema(Schema):
class RolePermissionsSchema(Schema):
id = fields.Str(dump_only=True)
role = fields.Nested(RoleSchema, only=('id', 'name', 'description'))
viewCourseLimited = fields.Bool()
viewCourseFull = fields.Bool()
updateCourse = fields.Bool()
setNotesAccessToken = fields.Bool()
assignRoles = fields.Bool()
writeRole = fields.Bool()
readRole = fields.Bool()
writeProject = fields.Bool()
deleteProject = fields.Bool()
archiveProject = fields.Bool()
writeGroup = fields.Bool()
readGroup = fields.Bool()
deleteGroup = fields.Bool()
readAllSubmissions = fields.Bool()
readOwnSubmissions = fields.Bool()
readGroupSubmissions = fields.Bool()
viewAllSubmissionFiles = fields.Bool()
createSubmission = fields.Bool()
resubmitSubmission = fields.Bool()
readAllReviews = fields.Bool()
readOwnReviews = fields.Bool()
writeOwnReview = fields.Bool()
writeAllReview = fields.Bool()
evaluateSubmission = fields.Bool()
view_course_limited = fields.Bool()
view_course_full = fields.Bool()
update_course = fields.Bool()
handle_notes_access_token = fields.Bool()
assign_roles = fields.Bool()
write_roles = fields.Bool()
read_roles = fields.Bool()
write_groups = fields.Bool()
read_groups = fields.Bool()
write_projects = fields.Bool()
read_projects = fields.Bool()
archive_projects = fields.Bool()
create_submissions = fields.Bool()
resubmit_submissions = fields.Bool()
evaluate_submissions = fields.Bool()
read_submissions_all = fields.Bool()
read_submissions_groups = fields.Bool()
read_submissions_own = fields.Bool()
read_all_submission_files = fields.Bool()
read_reviews_all = fields.Bool()
read_reviews_groups = fields.Bool()
read_reviews_own = fields.Bool()
write_reviews_all = fields.Bool()
write_reviews_group = fields.Bool()
write_reviews_own = fields.Bool()
class GroupSchema(Schema):
......@@ -178,6 +187,7 @@ class GroupImportSchema(Schema):
user_schema = UserSchema(strict=True)
user_schema_reduced = UserSchema(strict=True, only=('id', 'username', 'uco', 'email', 'name'))
users_schema = UserSchema(many=True, only=('id', 'username', 'uco', 'email'))
submission_schema = SubmissionSchema()
......@@ -193,6 +203,7 @@ roles_schema = RoleSchema(many=True, only=('id', 'name', 'description', 'course'
permissions_schema = RolePermissionsSchema()
course_schema = CourseSchema(strict=True)
course_schema_reduced = CourseSchema(strict=True, only=('id', 'name', 'codename', ))
courses_schema = CourseSchema(many=True, only=('id', 'name', 'codename'))
course_import_schema = CourseImportSchema(strict=True)
......
......@@ -3,37 +3,68 @@ from flask_jwt_extended import create_access_token, create_refresh_token, \
from flask_restful import Resource, Api
from flask import Blueprint, request, jsonify
from portal.service.auth import login_user
from portal.service.errors import UnauthorizedError
from portal import jwt
from portal.service.auth import login_user, login_component
from portal.service.errors import UnauthorizedError, PortalAPIError
from service import service
auth = Blueprint('auth', __name__, url_prefix='/auth')
auth_api = Api(auth)
@jwt.user_claims_loader
def add_claims_to_access_token(identity):
data = 'user'
if service.find_component(identity, throws=False):
data = 'component'
return {
'type': data
}
# basic login - username + password (user) / name + secret (component)
class Login(Resource):
def post(self):
username = request.get_json().get('username', None)
password = request.get_json().get('password', None)
gitlab_access_token = request.get_json().get('gitlab_access_token', None)
user = login_user(gitlab_access_token, password, username)
ret = {
'access_token': create_access_token(identity=user.id),
'refresh_token': create_refresh_token(identity=user.id)
data = request.get_json()
if not data.get('type'):
raise PortalAPIError(400, message="Missing login type.")
if data['type'] == 'user':
username = data.get('username')
password = data.get('password')
gitlab_access_token = data.get('gitlab_access_token', None)
client = login_user(gitlab_access_token, password, username)
elif data['type'] == 'component':
name = data.get('name')
if not name:
raise PortalAPIError(400, message="Missing component name.")
secret = data.get('secret')
if not secret:
raise PortalAPIError(400, message="Missing component secret.")
sender_ip_address = request.remote_addr
client = login_component(name, sender_ip_address, secret)
else:
raise PortalAPIError(400, message="Invalid login type.")
response = {
'access_token': create_access_token(identity=client.id),
'refresh_token': create_refresh_token(identity=client.id)
}
return ret, 200
return response, 200
class Refresh(Resource):
@jwt_refresh_token_required
def post(self):
current_user = get_jwt_identity()
if not current_user:
client = get_jwt_identity()
if not client:
raise UnauthorizedError()
ret = {
'access_token': create_access_token(identity=current_user)
'access_token': create_access_token(identity=client)
}
return jsonify(ret), 200
......
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from flask_restful import Api, Resource
from portal.database.models import Course
from portal.rest import course_schema, courses_schema, course_import_schema
from portal.service import service
from portal.service.errors import PortalAPIError
from portal.service.errors import PortalAPIError, ForbiddenError
from portal.tools.decorators import error_handler
from portal.tools.logging import log
from service import policies
courses = Blueprint('courses', __name__, url_prefix='/courses')
courses_api = Api(courses)
......@@ -14,22 +16,45 @@ courses_api = Api(courses)
class CourseResource(Resource):
@error_handler
@jwt_required
def get(self, cid):
log.info(f"GET to {request.url}")
client = service.find_client()
course = service.find_course(cid)
return course_schema.dump(course)
# authorization
if (policies.check_component(component=client, course=course, permissions=['view_course_full'])
or policies.check_user(user=client, course=course, permissions=['view_course_full'])):
return course_schema.dump(course)
elif (policies.check_component(component=client, course=course, permissions=['view_course_limited'])
or policies.check_user(user=client, course=course, permissions=['view_course_limited'])):
return course_schema.dump(service.filter_course_info(course, client))
raise ForbiddenError(uid=client.id)
@error_handler
@jwt_required
def delete(self, cid):
client = service.find_client()
log.info(f"DELETE to {request.url}")
course = service.find_course(cid)
service.delete_entity(course)
return '', 204
# authorization
if policies.check_sysadmin(client):
service.delete_entity(course)
return '', 204
raise ForbiddenError(uid=client.id)
@error_handler
@jwt_required
def put(self, cid):
log.info(f"PUT to {request.url}")
client = service.find_client()
course = service.find_course(cid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['update_course'])
or policies.check_user(user=client, course=course, permissions=['update_course'])):
raise ForbiddenError(uid=client.id)
json_data = request.get_json()
if not json_data:
raise PortalAPIError(400, message='No data provided for course update.')
......@@ -45,14 +70,26 @@ class CourseResource(Resource):
class CourseList(Resource):
@error_handler
@jwt_required
def get(self):
log.info(f"GET to {request.url}")
client = service.find_client()
# authorization
if not policies.check_sysadmin(client):
raise ForbiddenError(uid=client.id)
courses = Course.query.all()
return courses_schema.dump(courses)
@error_handler
@jwt_required
def post(self):
log.info(f"POST to {request.url}")
client = service.find_client()
# authorization
if not policies.check_sysadmin(client):
raise ForbiddenError(uid=client.id)
json_data = request.get_json()
if not json_data:
raise PortalAPIError(400, message='No data provided for course creation.')
......@@ -70,15 +107,29 @@ class CourseList(Resource):
class CourseNotesToken(Resource):
@error_handler
@jwt_required
def get(self, cid):
log.info(f"GET to {request.url}")
client = service.find_client()
course = service.find_course(cid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['handleNotesAccessToken'])
or policies.check_user(user=client, course=course, permissions=['handleNotesAccessToken'])):
raise ForbiddenError(uid=client.id)
return course.notes_access_token
@error_handler
@jwt_required
def put(self, cid):
log.info(f"PUT to {request.url}")
client = service.find_client()
course = service.find_course(cid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['handleNotesAccessToken'])
or policies.check_user(user=client, course=course, permissions=['handleNotesAccessToken'])):
raise ForbiddenError(uid=client.id)
json_data = request.get_json()
if not json_data:
raise PortalAPIError(400, message='No data provided for notes token update.')
......@@ -91,10 +142,17 @@ class CourseNotesToken(Resource):
class CourseImport(Resource):
@error_handler
@jwt_required
def put(self, cid):
# TODO: import from IS MU
log.info(f"PUT to {request.url}")
client = service.find_client()
course = service.find_course(cid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['update_course'])
or policies.check_user(user=client, course=course, permissions=['update_course'])):
raise ForbiddenError(uid=client.id)
json_data = request.get_json()
if not json_data:
raise PortalAPIError(400, message='No data provided for notes token update.')
......@@ -110,6 +168,6 @@ class CourseImport(Resource):
courses_api.add_resource(CourseResource, '/<string:cid>')
courses_api.add_resource(CourseList, '/')
courses_api.add_resource(CourseList, '')
courses_api.add_resource(CourseNotesToken, "/<string:cid>/notes_access_token")
courses_api.add_resource(CourseImport, "/<string:cid>/import")
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from flask_restful import Api, Resource
from portal.service import service
from portal.tools.decorators import error_handler
from portal.tools.logging import log
from portal.database.models import Group, Role
from portal.database.models import Group
from portal.rest import group_schema, groups_schema, users_schema, user_list_update_schema, group_import_schema
from portal.service.errors import PortalAPIError
from portal.service.errors import PortalAPIError, ForbiddenError
from service import policies
groups = Blueprint('groups', __name__, url_prefix='/courses/<string:cid>/groups')
groups_api = Api(groups)
......@@ -14,25 +16,48 @@ groups_api = Api(groups)
class GroupResource(Resource):
@error_handler
@jwt_required
def get(self, cid, gid):
log.info(f"GET to {request.url}")
client = service.find_client()
course = service.find_course(cid)
group = service.find_group(course, gid)
# authorization
# note: read_groups should be enabled for students of a course TODO make default?
if not (policies.check_component(component=client, course=course, permissions=['read_groups'])
or policies.check_user(user=client, course=course, permissions=['read_groups'])):
raise ForbiddenError(uid=client.id)
return group_schema.dump(group)
@error_handler
@jwt_required
def delete(self, cid, gid):
log.info(f"DELETE to {request.url}")
client = service.find_client()
course = service.find_course(cid)
group = service.find_group(course, gid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['write_groups'])
or policies.check_user(user=client, course=course, permissions=['write_groups'])):
raise ForbiddenError(uid=client.id)
service.delete_entity(group)
return '', 204
@error_handler
@jwt_required
def put(self, cid, gid):
log.info(f"PUT to {request.url}")
client = service.find_client()
course = service.find_course(cid)
group = service.find_group(course, gid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['write_groups'])
or policies.check_user(user=client, course=course, permissions=['write_groups'])):
raise ForbiddenError(uid=client.id)
json_data = request.get_json()
if not json_data:
raise PortalAPIError(400, message='No data provided for group update.')
......@@ -41,22 +66,35 @@ class GroupResource(Resource):
group.name = data['name']
service.write_entity(group)
log.debug(f"Updated group {gid} to {group}.")
log.debug(f"Updated group {group.name} to {group}.")
return '', 204
class GroupsList(Resource):
@error_handler
@jwt_required
def get(self, cid):
log.info(f"GET to {request.url}")
client = service.find_client()
course = service.find_course(cid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['read_groups'])
or policies.check_user(user=client, course=course, permissions=['read_groups'])):
raise ForbiddenError(uid=client.id)
return groups_schema.dump(course.groups)
@error_handler
@jwt_required
def post(self, cid):
log.info(f"POST to {request.url}")
client = service.find_client()
course = service.find_course(cid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['write_groups'])
or policies.check_user(user=client, course=course, permissions=['write_groups'])):
raise ForbiddenError(uid=client.id)
json_data = request.get_json()
if not json_data:
raise PortalAPIError(400, message='No data provided for group creation.')
......@@ -65,17 +103,23 @@ class GroupsList(Resource):
new_group = Group(course=course, name=data['name'])
service.write_entity(new_group)
log.info(f"Created group={new_group} for course {cid}")
log.info(f"Created group={new_group} for course {course.codename}")
return group_schema.dump(new_group)[0], 201
class GroupUsersList(Resource):
@error_handler
@jwt_required
def get(self, cid, gid):
log.info(f"GET to {request.url}")
client = service.find_client()
course = service.find_course(cid)
group = service.find_group(course, gid)
# authorization
if not (policies.check_component(component=client, course=course, permissions=['read_groups'])
or policies.check_user(user=client, course=course, permissions=['read_groups'])):
raise ForbiddenError(uid=client.id)