Loading portal/database/__init__.py +12 −0 Original line number Diff line number Diff line 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 portal/database/models.py +36 −36 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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: Loading Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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)) Loading portal/rest/__init__.py +42 −31 Original line number Diff line number Diff line 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) Loading Loading @@ -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')) Loading Loading @@ -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): Loading Loading @@ -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() Loading @@ -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) Loading portal/rest/auth/login.py +46 −15 Original line number Diff line number Diff line Loading @@ -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 Loading portal/rest/courses/courses.py +63 −5 Original line number Diff line number Diff line 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) Loading @@ -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.') Loading @@ -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.') Loading @@ -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.') Loading @@ -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.') Loading @@ -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") Loading
portal/database/__init__.py +12 −0 Original line number Diff line number Diff line 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
portal/database/models.py +36 −36 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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: Loading Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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)) Loading
portal/rest/__init__.py +42 −31 Original line number Diff line number Diff line 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) Loading Loading @@ -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')) Loading Loading @@ -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): Loading Loading @@ -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() Loading @@ -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) Loading
portal/rest/auth/login.py +46 −15 Original line number Diff line number Diff line Loading @@ -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 Loading
portal/rest/courses/courses.py +63 −5 Original line number Diff line number Diff line 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) Loading @@ -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.') Loading @@ -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.') Loading @@ -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.') Loading @@ -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.') Loading @@ -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")