Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Kontr 2.0
Portal API Backend
Commits
ee0dd3a7
Commit
ee0dd3a7
authored
May 27, 2018
by
Barbora Kompišová
Browse files
Migrated to flask-restplus
parent
b9372505
Pipeline
#12778
passed with stage
in 7 minutes and 39 seconds
Changes
44
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Pipfile
View file @
ee0dd3a7
...
...
@@ -7,7 +7,6 @@ name = "pypi"
flask
=
"*"
flask-sqlalchemy
=
"*"
pytest
=
"*"
flask-restful
=
"*"
marshmallow
=
"*"
flask-jwt-extended
=
"*"
marshmallow-enum
=
"*"
...
...
@@ -22,6 +21,7 @@ pytz = "*"
flask-migrate
=
"*"
python-gitlab
=
"*"
psycopg2-binary
=
"*"
flask-restplus
=
"*"
[dev-packages]
pytest-cov
=
"*"
...
...
portal/__init__.py
View file @
ee0dd3a7
...
...
@@ -94,5 +94,5 @@ def create_app(environment: str = None):
configure_app
(
app
,
env
=
environment
)
configure_storage
(
app
)
configure_extensions
(
app
)
rest
.
register_
blueprint
s
(
app
)
rest
.
register_
namespace
s
(
app
)
return
app
portal/database/exceptions.py
View file @
ee0dd3a7
from
sqlalchemy.exc
import
SQLAlchemyError
"""
Database specific exceptions
"""
from
sqlalchemy.exc
import
SQLAlchemyError
class
PortalDbError
(
SQLAlchemyError
):
"""Raised when a forbidden operation on portal's database is attempted.
"""
\ No newline at end of file
"""
portal/database/mixins.py
View file @
ee0dd3a7
from
portal
import
db
"""
A collection of
m
ixins specifying common behaviour and attributes of database entities.
A collection of
M
ixins specifying common behaviour and attributes of database entities.
"""
from
portal
import
db
# maybe use server_default, server_onupdate instead of default, onupdate; crashes tests
# (https://stackoverflow.com/questions/13370317/sqlalchemy-default-datetime)
...
...
portal/database/models.py
View file @
ee0dd3a7
"""
Models module where all of the models are specified
"""
import
enum
import
uuid
from
flask_sqlalchemy
import
BaseQuery
...
...
@@ -15,10 +19,6 @@ from portal.tools import time
# https://stackoverflow.com/questions/36806403/cant-render-element-of-type-class-sqlalchemy-dialects-postgresql-base-uuid
from
portal.tools.time
import
normalize_time
"""
Models module where all of the models are specified
"""
def
_repr
(
instance
)
->
str
:
"""Repr helper function
...
...
@@ -467,21 +467,40 @@ class ProjectConfig(db.Model, EntityBase):
@
hybrid_property
def
submissions_allowed_from
(
self
):
"""Gets from which date all the submissions are allowed from
Returns(time): Time from submissions are allowed from
"""
seconds
=
time
.
strip_seconds
(
self
.
_submissions_allowed_from
)
return
seconds
@
hybrid_property
def
submissions_allowed_to
(
self
):
"""Gets from which date all the submissions are allowed to
Returns(time): Time from submissions are allowed to
"""
seconds
=
time
.
strip_seconds
(
self
.
_submissions_allowed_to
)
return
seconds
@
hybrid_property
def
archive_from
(
self
):
"""Gets from which date all the submissions are archived from
Returns(time): Time from submissions should be archived from
"""
seconds
=
time
.
strip_seconds
(
self
.
_archive_from
)
return
seconds
@
submissions_allowed_from
.
setter
def
submissions_allowed_from
(
self
,
submissions_allowed_from
):
"""Set time from which submissions are allowed to submit
Args:
submissions_allowed_from(time): Time from which all the submissions are allowed to submit
"""
submissions_allowed_from
=
normalize_time
(
submissions_allowed_from
)
if
self
.
submissions_allowed_to
is
not
None
and
\
submissions_allowed_from
>
self
.
submissions_allowed_to
:
...
...
@@ -493,6 +512,10 @@ class ProjectConfig(db.Model, EntityBase):
@
submissions_allowed_to
.
setter
def
submissions_allowed_to
(
self
,
submissions_allowed_to
):
"""Set time from which submissions are allowed to submit
Args:
submissions_allowed_to(time): Time to which all the submissions are allowed to submit
"""
submissions_allowed_to
=
normalize_time
(
submissions_allowed_to
)
if
self
.
submissions_allowed_from
is
not
None
and
\
self
.
submissions_allowed_from
>
submissions_allowed_to
:
...
...
@@ -504,6 +527,10 @@ class ProjectConfig(db.Model, EntityBase):
@
archive_from
.
setter
def
archive_from
(
self
,
archive_from
):
"""Set time from which submissions are allowed to archive
Args:
submissions_allowed_from(time): Time from which all the submissions are allowed to archive
"""
archive_from
=
normalize_time
(
archive_from
)
if
self
.
submissions_allowed_from
is
not
None
and
\
self
.
submissions_allowed_from
>
archive_from
:
...
...
portal/database/utils.py
View file @
ee0dd3a7
"""
Utils to help with database
"""
from
flask_sqlalchemy
import
BaseQuery
from
sqlalchemy
import
func
...
...
portal/logging.py
View file @
ee0dd3a7
"""
Logging configuration module
"""
from
logging.config
import
dictConfig
# look into: https://www.packetmischief.ca/2017/10/25/3-ways-to-fail-at-logging-with-flask/
# source: https://docs.python.org/3/howto/logging-cookbook.html (An example dictionary-based configuration)
# discussion: https://github.com/pallets/flask/issues/2023
FORMATTERS
=
{
'verbose'
:
{
'format'
:
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
...
...
portal/rest/__init__.py
View file @
ee0dd3a7
from
flask
import
Flask
"""
Rest layer module
"""
from
flask
import
Flask
from
flask_restplus
import
Api
API_PREFIX
=
"/api/v1.0"
def
register_blueprints
(
app
:
Flask
):
def
create_api
():
return
Api
(
title
=
'Kontr Portal API'
,
version
=
'1.0'
,
description
=
'Kontr Portal API'
,
doc
=
f
"
{
API_PREFIX
}
/docs"
,
prefix
=
API_PREFIX
)
rest_api
=
create_api
()
def
register_namespaces
(
app
:
Flask
):
"""Registers blue prints for the application
Args:
app(Flask): Flask application
Returns(Flask): Flask application
"""
from
portal.rest.auth.login
import
auth_blueprint
from
portal.rest.courses.courses
import
courses_blueprint
from
portal.rest.roles.roles
import
roles_blueprint
from
portal.rest.groups.groups
import
groups_blueprint
from
portal.rest.submissions.submissions
import
submissions_blueprint
from
portal.rest.projects.projects
import
projects_blueprint
from
portal.rest.components.components
import
components_blueprint
from
portal.rest.notifications.notifications
import
notifications_blueprint
from
portal.rest.users.users
import
users_blueprint
from
portal.rest.management.management
import
management_blueprint
app
.
register_blueprint
(
auth_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/auth"
)
app
.
register_blueprint
(
users_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/users"
)
app
.
register_blueprint
(
courses_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/courses"
)
app
.
register_blueprint
(
roles_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/courses/<string:cid>/roles"
)
app
.
register_blueprint
(
groups_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/courses/<string:cid>/groups"
)
app
.
register_blueprint
(
projects_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/courses/<string:cid>/projects"
)
app
.
register_blueprint
(
submissions_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/submissions"
)
app
.
register_blueprint
(
components_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/components"
)
app
.
register_blueprint
(
notifications_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/notifications"
)
app
.
register_blueprint
(
management_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/management"
)
__register_gitlab_endpoint
(
app
)
return
app
from
portal.rest.auth.login
import
auth_namespace
from
portal.rest.courses.courses
import
courses_namespace
from
portal.rest.roles.roles
import
roles_namespace
from
portal.rest.groups.groups
import
groups_namespace
from
portal.rest.submissions.submissions
import
submissions_namespace
from
portal.rest.projects.projects
import
projects_namespace
from
portal.rest.components.components
import
components_namespace
from
portal.rest.notifications.notifications
import
notifications_namespace
from
portal.rest.users.users
import
users_namespace
from
portal.rest.management.management
import
management_namespace
rest_api
.
add_namespace
(
auth_namespace
)
rest_api
.
add_namespace
(
courses_namespace
)
rest_api
.
add_namespace
(
roles_namespace
)
rest_api
.
add_namespace
(
groups_namespace
)
rest_api
.
add_namespace
(
submissions_namespace
)
rest_api
.
add_namespace
(
projects_namespace
)
rest_api
.
add_namespace
(
components_namespace
)
rest_api
.
add_namespace
(
notifications_namespace
)
rest_api
.
add_namespace
(
users_namespace
)
rest_api
.
add_namespace
(
management_namespace
)
def
__register_gitlab_endpoint
(
app
:
Flask
):
"""Registers the gitlab endpoint only if gitlab url has been specified
Args:
app(Flask): Flask application
"""
if
app
.
config
.
get
(
'GITLAB_URL'
):
from
portal.rest.auth.gitlab
import
oauth_blueprint
app
.
register_blueprint
(
oauth_blueprint
,
url_prefix
=
f
"
{
API_PREFIX
}
/oauth"
)
from
portal.rest.auth.gitlab
import
oauth_namespace
rest_api
.
add_namespace
(
management_namespace
)
rest_api
.
init_app
(
app
)
import
portal.rest.errors
return
app
portal/rest/auth/gitlab.py
View file @
ee0dd3a7
import
logging
from
flask
import
Blueprint
,
url_for
,
request
,
redirect
,
session
,
make_response
,
Flask
,
Response
from
flask
import
Flask
,
Response
,
make_response
,
redirect
,
request
,
session
,
url_for
from
flask_oauthlib.client
import
OAuth
,
OAuthRemoteApp
from
flask_restplus
import
Namespace
,
Resource
from
typing
import
Union
from
portal
import
oauth
...
...
@@ -11,6 +11,8 @@ from portal.tools.decorators import error_handler
log
=
logging
.
getLogger
(
__name__
)
oauth_namespace
=
Namespace
(
'oauth'
)
def
extract_user_info
(
me
:
dict
)
->
dict
:
log
.
debug
(
f
"[GITLAB] Received info:
{
me
}
"
)
...
...
@@ -49,18 +51,18 @@ def create_gitlab_app(oauth: OAuth) -> Union[OAuthRemoteApp, None]:
gitlab
=
create_gitlab_app
(
oauth
=
oauth
)
oauth_blueprint
=
Blueprint
(
'oauth'
,
__name__
,
url_prefix
=
'/oauth'
)
@
error_handler
@
oauth_blueprint
.
route
(
'/login'
,
methods
=
[
'GET'
])
def
oauth_login
():
if
not
gitlab
:
return
{
'message'
:
'Gitlab OAuth is not enabled'
},
404
@
oauth_namespace
.
route
(
'/login'
)
class
OAuthLogin
(
Resource
):
def
get
(
self
):
if
not
gitlab
:
return
{
'message'
:
'Gitlab OAuth is not enabled'
},
404
callback
=
url_for
(
'oauth.oauth_authorized'
,
_external
=
True
,
_scheme
=
'https'
)
log
.
debug
(
f
"Callback set:
{
callback
}
"
)
return
gitlab
.
authorize
(
callback
=
callback
)
callback
=
url_for
(
'oauth.oauth_authorized'
,
_external
=
True
,
_scheme
=
'https'
)
log
.
debug
(
f
"Callback set:
{
callback
}
"
)
return
gitlab
.
authorize
(
callback
=
callback
)
def
user_oauth_register
(
user_info
):
...
...
@@ -84,27 +86,28 @@ def user_login(user_info) -> Response:
return
resp
@
error_handler
@
oauth_blueprint
.
route
(
'/login/authorized'
,
methods
=
[
'GET'
])
def
oauth_authorized
():
if
not
gitlab
:
return
{
'message'
:
'Gitlab OAuth is not enabled'
},
404
resp
=
gitlab
.
authorized_response
()
if
resp
is
None
:
return
'Access denied: reason=%s error=%s'
%
(
request
.
args
[
'error'
],
request
.
args
[
'error_description'
]
)
token
=
resp
[
'access_token'
]
log
.
debug
(
f
"[GITLAB] Received token:
{
token
}
"
)
session
[
'gitlab_token'
]
=
(
token
,
''
)
me
=
gitlab
.
get
(
'/api/v4/user'
)
user_info
=
extract_user_info
(
me
.
data
)
login
=
user_login
(
user_info
)
login
.
set_cookie
(
'gitlab_token'
,
token
)
log
.
debug
(
f
"[GITLAB] Gitlab Login response:
{
login
}
"
)
return
login
@
oauth_namespace
.
route
(
'/login/authorized'
)
class
OAuthLoginAuthorized
(
Resource
):
def
get
(
self
):
if
not
gitlab
:
return
{
'message'
:
'Gitlab OAuth is not enabled'
},
404
resp
=
gitlab
.
authorized_response
()
if
resp
is
None
:
return
'Access denied: reason=%s error=%s'
%
(
request
.
args
[
'error'
],
request
.
args
[
'error_description'
]
)
token
=
resp
[
'access_token'
]
log
.
debug
(
f
"[GITLAB] Received token:
{
token
}
"
)
session
[
'gitlab_token'
]
=
(
token
,
''
)
me
=
gitlab
.
get
(
'/api/v4/user'
)
user_info
=
extract_user_info
(
me
.
data
)
login
=
user_login
(
user_info
)
login
.
set_cookie
(
'gitlab_token'
,
token
)
log
.
debug
(
f
"[GITLAB] Gitlab Login response:
{
login
}
"
)
return
login
@
gitlab
.
tokengetter
...
...
portal/rest/auth/login.py
View file @
ee0dd3a7
import
logging
from
flask_jwt_extended
import
create_access_token
,
create_refresh_token
,
\
jwt_refresh_token_required
,
get_jwt_identity
,
jwt_required
from
flask_restful
import
Resource
,
Api
from
flask
import
Blueprint
,
request
from
flask
import
request
from
flask_jwt_extended
import
create_access_token
,
create_refresh_token
,
get_jwt_identity
,
\
jwt_refresh_token_required
,
jwt_required
from
flask_restplus
import
Resource
,
Namespace
from
portal
import
jwt
from
portal.service
import
general
from
portal.service.auth
import
login_
user
,
login_component
from
portal.service.errors
import
UnauthorizedError
,
PortalAPIError
from
portal.service.auth
import
login_
component
,
login_user
from
portal.service.errors
import
PortalAPIError
,
UnauthorizedError
from
portal.tools.decorators
import
error_handler
log
=
logging
.
getLogger
(
__name__
)
auth_blueprint
=
Blueprint
(
'auth'
,
__name__
,
url_prefix
=
'/auth'
)
auth_api
=
Api
(
auth_blueprint
)
auth_namespace
=
Namespace
(
'auth'
)
@
jwt
.
user_claims_loader
...
...
@@ -24,12 +22,13 @@ def add_claims_to_access_token(identity):
data
=
'component'
return
{
'type'
:
data
}
}
# basic login - username + password (user) / name + secret (component)
@
auth_namespace
.
route
(
'/login'
)
class
Login
(
Resource
):
@
error_handler
def
post
(
self
):
data
=
request
.
get_json
()
if
not
data
.
get
(
'type'
):
...
...
@@ -59,12 +58,13 @@ class Login(Resource):
id
=
client
.
id
,
access_token
=
create_access_token
(
identity
=
client
.
id
),
refresh_token
=
create_refresh_token
(
identity
=
client
.
id
)
)
)
return
response
,
200
@
auth_namespace
.
route
(
'/refresh'
)
class
Refresh
(
Resource
):
@
error_handler
@
jwt_refresh_token_required
def
post
(
self
):
client
=
get_jwt_identity
()
...
...
@@ -73,13 +73,14 @@ class Refresh(Resource):
ret
=
dict
(
access_token
=
create_access_token
(
identity
=
client
)
)
)
return
ret
,
200
@
auth_namespace
.
route
(
'/logout'
)
class
Logout
(
Resource
):
@
error_handler
@
jwt_required
def
post
(
self
):
# might use redis later
...
...
@@ -91,11 +92,6 @@ class Logout(Resource):
ret
=
{
'access_token'
:
None
,
'refresh_token'
:
None
,
}
}
return
ret
,
200
auth_api
.
add_resource
(
Login
,
'/login'
)
auth_api
.
add_resource
(
Refresh
,
'/refresh'
)
auth_api
.
add_resource
(
Logout
,
'/logout'
)
portal/rest/components/components.py
View file @
ee0dd3a7
import
logging
from
flask
import
Blueprint
from
flask_jwt_extended
import
jwt_required
from
flask_rest
ful
import
Api
,
Resource
from
flask_rest
plus
import
Resource
,
Namespace
from
portal.rest
import
rest_helpers
from
portal.rest.schemas
import
component_schema
,
components_schema
from
portal.service
import
permissions
,
auth
from
portal.service
import
auth
,
permissions
from
portal.service.auth
import
find_client
from
portal.service.components
import
create_component
,
delete_component
,
update
_component
,
\
find_all
_component
s
from
portal.service.components
import
create_component
,
delete_component
,
find_all
_component
s
,
\
update
_component
from
portal.service.errors
import
ForbiddenError
from
portal.service.general
import
find_component
from
portal.tools.decorators
import
error_handler
components_blueprint
=
Blueprint
(
'components'
,
__name__
)
components_api
=
Api
(
components_blueprint
)
components_namespace
=
Namespace
(
'components'
)
log
=
logging
.
getLogger
(
__name__
)
@
components_namespace
.
route
(
''
)
class
ComponentList
(
Resource
):
@
error_handler
@
jwt_required
def
get
(
self
):
client
=
auth
.
find_client
()
...
...
@@ -30,7 +28,7 @@ class ComponentList(Resource):
components_list
=
find_all_components
()
return
components_schema
.
dump
(
components_list
)[
0
],
200
@
error_handler
@
jwt_required
def
post
(
self
):
client
=
auth
.
find_client
()
...
...
@@ -40,15 +38,17 @@ class ComponentList(Resource):
data
=
rest_helpers
.
parse_request_data
(
component_schema
,
action
=
'create'
,
resource
=
'component'
)
)
new_component
=
create_component
(
**
data
)
return
component_schema
.
dump
(
new_component
)[
0
],
201
@
components_namespace
.
route
(
'/<string:cid>'
)
@
components_namespace
.
doc
(
params
=
{
'cid'
:
'An Component ID'
})
class
ComponentResource
(
Resource
):
@
error_handler
@
jwt_required
def
get
(
self
,
cid
):
def
get
(
self
,
cid
:
str
):
client
=
find_client
()
if
not
permissions
.
check_sysadmin
(
client
):
raise
ForbiddenError
(
uid
=
client
.
id
)
...
...
@@ -56,9 +56,9 @@ class ComponentResource(Resource):
component
=
find_component
(
cid
)
return
component_schema
.
dump
(
component
)[
0
],
200
@
error_handler
@
jwt_required
def
delete
(
self
,
cid
):
def
delete
(
self
,
cid
:
str
):
client
=
find_client
()
# authorization
if
not
permissions
.
check_sysadmin
(
client
):
...
...
@@ -68,9 +68,9 @@ class ComponentResource(Resource):
delete_component
(
component
)
return
''
,
204
@
error_handler
@
jwt_required
def
put
(
self
,
cid
):
def
put
(
self
,
cid
:
str
):
client
=
find_client
()
# authorization
if
not
permissions
.
check_sysadmin
(
client
):
...
...
@@ -78,12 +78,8 @@ class ComponentResource(Resource):
data
=
rest_helpers
.
parse_request_data
(
component_schema
,
action
=
'update'
,
resource
=
'component'
)
)
component
=
find_component
(
cid
)
update_component
(
component
,
data
)
return
''
,
204
components_api
.
add_resource
(
ComponentList
,
''
)
components_api
.
add_resource
(
ComponentResource
,
'/<string:cid>'
)
portal/rest/courses/courses.py
View file @
ee0dd3a7
import
logging
from
flask
import
Blueprint
,
request
from
flask
import
request
from
flask_jwt_extended
import
jwt_required
from
flask_rest
ful
import
Api
,
Resource
from
flask_rest
plus
import
Namespace
,
Resource
from
portal.rest
import
rest_helpers
from
portal.rest.schemas
import
course_schema
,
courses_schema
,
course_import_schema
,
users_schema
from
portal.service.courses
import
delete_course
,
update_course
,
create_course
,
\
update_notes_token
,
copy_course
,
find_all_courses
,
get_users_filtered
from
portal.rest.schemas
import
course_import_schema
,
course_schema
,
courses_schema
,
users_schema
from
portal.service
import
permissions
from
portal.service.auth
import
find_client
from
portal.service.courses
import
copy_course
,
create_course
,
delete_course
,
find_all_courses
,
\
get_users_filtered
,
update_course
,
update_notes_token
from
portal.service.errors
import
ForbiddenError
,
PortalAPIError
from
portal.service.filters
import
filter_course_dump
from
portal.service.general
import
find_course
from
portal.service.errors
import
ForbiddenError
,
PortalAPIError
from
portal.service.permissions
import
check_client
from
portal.service.auth
import
find_client
from
portal.tools.decorators
import
error_handler
from
portal.service
import
permissions
courses_blueprint
=
Blueprint
(
'courses'
,
__name__
)
courses_api
=
Api
(
courses_blueprint
)
courses_namespace
=
Namespace
(
'courses'
)
log
=
logging
.
getLogger
(
__name__
)
@
courses_namespace
.
route
(
''
)
class
CourseList
(
Resource
):
@
jwt_required
def
get
(
self
):
client
=
find_client
()
# authorization
if
not
permissions
.
check_sysadmin
(
client
):
raise
ForbiddenError
(
uid
=
client
.
id
)
courses_list
=
find_all_courses
()
return
courses_schema
.
dump
(
courses_list
)
@
jwt_required
def
post
(
self
):
client
=
find_client
()
# authorization
if
not
permissions
.
check_sysadmin
(
client
):
raise
ForbiddenError
(
uid
=
client
.
id
)
data
=
rest_helpers
.
parse_request_data
(
course_schema
,
resource
=
'course'
,
action
=
'create'
)
new_course
=
create_course
(
**
data
)
return
course_schema
.
dump
(
new_course
)[
0
],
201
@
courses_namespace
.
route
(
'/<string:cid>'
)
@
courses_namespace
.
doc
({
'cid'
:
'Course id'
})
class
CourseResource
(
Resource
):
@
error_handler
@
jwt_required
def
get
(
self
,
cid
):
def
get
(
self
,
cid
:
str
):
client
=
find_client
()
course
=
find_course
(
cid
)
# authorization
...
...
@@ -43,9 +68,9 @@ class CourseResource(Resource):
raise
ForbiddenError
(
uid
=
client
.
id
)
@
error_handler
@
jwt_required
def
delete
(
self
,
cid
):
def
delete
(
self
,
cid
:
str
):
client
=
find_client
()
# authorization
if
not
permissions
.
check_sysadmin
(
client
):
...
...
@@ -55,9 +80,9 @@ class CourseResource(Resource):
delete_course
(
course
)