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

Authorization + login&tests refactor

parent 0052e7df
Loading
Loading
Loading
Loading
+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
+36 −36
Original line number Diff line number Diff line
@@ -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))


+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)

@@ -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)

+46 −15
Original line number Diff line number Diff line
@@ -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
+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)
@@ -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")
Loading