Verified Commit 9a32bcd4 authored by Peter Stanko's avatar Peter Stanko
Browse files

Add gitlab_url as cookie

parent a866d4e9
......@@ -16,7 +16,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
from portal import db
from portal.database.exceptions import PortalDbError
from portal.database.mixins import EntityBase, NamedMixin
from portal.database.types import JSONEncodedDict
from portal.database.types import JSONEncodedDict, YAMLEncodedDict
from portal.tools import time
from portal.tools.time import normalize_time
......@@ -580,10 +580,10 @@ class ProjectConfig(db.Model, EntityBase):
test_files_subdir = db.Column(db.String(50)) # git subdir
test_files_commit_hash = db.Column(db.String(128)) # hash of the latest revision of test_files
file_whitelist = db.Column(db.Text)
pre_submit_script = db.Column(db.Text)
post_submit_script = db.Column(db.Text)
submission_parameters = db.Column(db.Text)
submission_scheduler_config = db.Column(db.Text)
pre_submit_script = db.Column(YAMLEncodedDict)
post_submit_script = db.Column(YAMLEncodedDict)
submission_parameters = db.Column(YAMLEncodedDict)
submission_scheduler_config = db.Column(YAMLEncodedDict)
_submissions_allowed_from = db.Column(db.TIMESTAMP(timezone=True))
_submissions_allowed_to = db.Column(db.TIMESTAMP(timezone=True))
......
import json
import sqlalchemy
import yaml
from sqlalchemy import TypeDecorator
......@@ -18,3 +19,19 @@ class JSONEncodedDict(TypeDecorator):
if value is not None:
value = json.loads(value)
return value
class YAMLEncodedDict(TypeDecorator):
"Represents an immutable structure as a yaml-encoded string."
impl = sqlalchemy.Text
def process_bind_param(self, value, dialect):
if value is not None:
value = yaml.safe_dump(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = yaml.safe_load(value)
return value
......@@ -106,6 +106,7 @@ class GitlabFacade(GeneralFacade):
'gitlab_token_type': token_type,
'gitlab_username': user_info.username,
'username': user_info.username,
'gitlab_url': self._config.get('GITLAB_URL')
}
return flask.jsonify(params) if not redir else self._make_login_response(params)
......
......@@ -9,6 +9,25 @@ from portal.service.projects import ProjectService
log = logging.getLogger(__name__)
def _check_gl_repo_size(gl_project, project):
max_repo_size = project.config.submission_scheduler_config.get('repo_max_size')
if max_repo_size:
repo_size = gl_project.statistics['repository_size']
if repo_size > max_repo_size:
raise errors.SubmissionRefusedError(f"Your repository is bigger than the limit:"
f" {repo_size} > {max_repo_size}")
def _check_gl_repo_visibility(gl_project, project):
required_repo_visibility = project.config.submission_scheduler_config.get('repo_visibility')
if required_repo_visibility:
repo_visibility = gl_project.visibility
if required_repo_visibility != repo_visibility:
raise errors.SubmissionRefusedError(f"Your repository visibility "
f"is not {required_repo_visibility}."
f" Actual: {repo_visibility}")
class ProjectsFacade(GeneralCRUDFacade):
def __init__(self):
super().__init__(ProjectService, 'Project')
......@@ -67,6 +86,8 @@ class ProjectsFacade(GeneralCRUDFacade):
"""
client = client or self.client
result = self._service(project).check_submission_create(client)
if self._services.kontr_gitlab.is_authorized and not project.submit_configurable:
result = self._check_gitlab_params(client, project)
return result
def get_test_files(self, project: Project):
......@@ -100,3 +121,11 @@ class ProjectsFacade(GeneralCRUDFacade):
f"{self.client_name}")
result = self._services.is_api(project.course).create_notepad(project=project)
return result
def _check_gitlab_params(self, client: Client, project: Project):
gl_repo = self.services.kontr_gitlab.build_repo_name(client, project)
gl_project = self._services.kontr_gitlab.get_project(gl_repo)
_check_gl_repo_size(gl_project, project)
_check_gl_repo_visibility(gl_project, project)
return True
import flask
from flask import request
from flask_jwt_extended import jwt_required
from flask_restplus import Namespace
......@@ -209,6 +210,37 @@ class ProjectSubmissions(CustomResource):
self.facades.projects.check_submission_create(project, client=user)
@projects_namespace.route(
'/courses/<string:cid>/projects/<string:pid>/submissions/check')
@projects_namespace.param('cid', 'Course id')
@projects_namespace.param('pid', 'Project id')
@projects_namespace.response(404, 'Course not found')
@projects_namespace.response(404, 'Project not found')
class ProjectSubmissionsCheck(CustomResource):
@jwt_required
@access_log
# @projects_namespace.response(201, 'Project submission create', model=submission_schema)
def get(self, cid: str, pid: str):
course = self.find.course(cid)
user = request.args.get('user')
user = self.find.user(user) if user is not None else self.client
# authorization
self.permissions(course=course).require.create_submission(user)
# check if a new submission can be created by this user in this project
project = self.find.project(course, pid)
self._check_user_can_create_submission(project, user)
log.info(f"[REST] Check Create submission in {project.log_name} "
f"by {self.client.log_name} for user {user.log_name}")
return flask.jsonify({'message': 'User is able to create submission'}), 200
def _check_user_can_create_submission(self, project, user):
if self.client.type == ClientType.USER:
client_instance = self.find.user(self.client.id)
if not client_instance.is_admin:
self.facades.projects.check_submission_create(project, client=user)
@projects_namespace.route('/courses/<string:cid>/projects/<string:pid>/files')
@projects_namespace.param('cid', 'Course id')
@projects_namespace.param('pid', 'Project id')
......
......@@ -2,11 +2,14 @@
Errors in the service layer
"""
import json
import logging
from typing import Union
from portal.database import Course, Project, User
from portal.tools import time
log = logging.getLogger(__name__)
class PortalError(Exception):
"""Base exception class
......@@ -37,6 +40,7 @@ class PortalAPIError(PortalError):
super().__init__()
self._code = code
self._message = message
log.error(f"[ERR] API ERROR[{code}]: {message}")
@property
def code(self) -> int:
......
import logging
from typing import List, Optional
from urllib.parse import urlparse
import gitlab
from gitlab.v4 import objects
from portal import tools
from portal.database import Submission
from portal.database import Submission, models
from portal.service import errors
from portal.service.general import GeneralService
......@@ -34,8 +35,7 @@ class GitlabService(GeneralService):
@property
def gl_client(self):
if self._client is None:
log.debug(f"[GL_CLIENT] Creating GL client for "
f"token[{self.token_type}]: {self.token}")
log.debug(f"[GL_CLIENT] Creating GL client for token type - [{self.token_type}]")
self._client = self.get_client(token=self._token, token_type=self._token_type)
return self._client
......@@ -53,7 +53,8 @@ class GitlabService(GeneralService):
params[f'{token_type}_token'] = token
gitlab_url = self._config.get('GITLAB_URL')
instance: gitlab.Gitlab = gitlab.Gitlab(gitlab_url, **params)
log.debug(f"[CL_CLIENT] Instance {instance.url} - {params}")
reducted_params = {(k, v) for (k, v) in params.items() if not k.endswith('_token')}
log.debug(f"[CL_CLIENT] Instance {instance.url} - {reducted_params}")
try:
if token:
......@@ -149,6 +150,16 @@ class GitlabService(GeneralService):
return None
return full_name
@property
def git_domain(self) -> str:
return self._config.get('GITLAB_BASE_DOMAIN') or urlparse(self.gl_client.url).hostname
def build_ssh_url(self, client: models.Client, project: models.Project):
return f"git@{self.git_domain}:{self.build_repo_name(client, project)}.git"
def build_repo_name(self, client: models.Client, project: models.Project):
return f"{client.codename}/{project.course.codename}"
class KontrGitlabService(GitlabService):
def __init__(self):
......
......@@ -137,7 +137,8 @@ class ProjectService(GeneralService):
if not ProjectService(self.project).can_create_submission(client):
log.warning(f"[WARN] Already active submission for: {self.project.log_name}")
raise errors.SubmissionRefusedError(
f"Submission for project {self.project.log_name} already active.")
f"User {user.log_name} has already active "
f"submission for project {self.project.log_name}")
self._check_latest_submission(user)
return True
......
......@@ -231,8 +231,9 @@ def test_set_gitlab_service_cookies(client, gl_api_url, gl_user, gl_cookies):
response = rest_tools.make_request(client, f'/gitlab/service_token')
assert_response(response)
res_proj = rest_tools.extract_data(response)
assert len(res_proj) == 4
assert len(res_proj) == 5
assert res_proj['username'] == gl_cookies['username']
assert res_proj['gitlab_username'] == gl_cookies['gitlab_username']
assert res_proj['gitlab_token'] == gl_cookies['gitlab_token']
assert res_proj['gitlab_token_type'] == gl_cookies['gitlab_token_type']
assert res_proj['gitlab_url']
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment