Skip to content

Commit

Permalink
Merge pull request #1341 from ScilifelabDataCentre/dev
Browse files Browse the repository at this point in the history
New release: v2.2.4
  • Loading branch information
i-oden authored Dec 14, 2022
2 parents f6fdb67 + 170c3a0 commit 49ca6e7
Show file tree
Hide file tree
Showing 19 changed files with 1,368 additions and 2,940 deletions.
37 changes: 23 additions & 14 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@
# Description

- [ ] Add a summary of the changes and the related issue
- [ ] Add motivation and context regarding why the change is needed
- [ ] List / describe any dependencies or other changes required for this change
- [ ] Fixes [link to issue / Jira issue ID]
- [ ] Summary of the changes and the related issue:
- [ ] Motivation and context regarding why the change is needed:
- [ ] List / description of any dependencies or other changes required for this change:
- Fixes an issue in GitHub / Jira:
- [ ] Yes: _[link to GitHub issue / Jira task ID]_
- [ ] No

## Type of change

- [ ] Bug fix
- [ ] Breaking: _Describe_
- [ ] Non-breaking
- [ ] Documentation
- [ ] Workflow
- [ ] New feature
- [ ] Breaking: _Describe_
- [ ] Non-breaking
- [ ] Security Alert fix
- [ ] Bug fix (non-breaking)
- [ ] New feature (non-breaking)
- [ ] Breaking change (breaking, will cause existing functionality to not work as expected)
- [ ] Tests (only)
- [ ] Tests **(only)**
- [ ] Workflow

_"Breaking": The change will cause existing functionality to not work as expected._

# Checklist:

Expand All @@ -30,7 +37,9 @@
- [ ] Code change
- [ ] Self-review of code done
- [ ] Comments added, particularly in hard-to-understand areas
- [ ] Documentation is updated
- Documentation update
- [ ] Done
- [ ] Not needed

## Repository / Releases

Expand All @@ -40,18 +49,18 @@
- [ ] The [version](../dds_web/version.py) is updated
- [ ] I am bumping the major version (e.g. 1.x.x to 2.x.x)
- [ ] I have made the corresponding changes to the CLI version
- [ ] Backward compatible
- Backward compatible
- [ ] Yes: The code works together with `dds_cli/master` branch
- [ ] No: The code **does not** entirely / at all work together with the `dds_cli/master` branch. _Please add detailed and clear information about the broken features_

## Checks

- [ ] Formatting: Black & Prettier checks pass
- [ ] CodeQL passes
- [ ] Tests
- [ ] Formatting: Black & Prettier checks pass
- Tests
- [ ] I have added tests for the new code
- [ ] The tests pass
- [ ] Trivy / Snyk:
- Trivy / Snyk:
- [ ] There are no new security alerts
- [ ] This PR fixes new security alerts
- [ ] Security alerts have been dismissed
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,11 @@ Please add a _short_ line describing the PR you make, if the PR implements a spe
## Sprint (2022-11-25 - 2022-12-09)

- Changed support email ([#1324](https://github.com/ScilifelabDataCentre/dds_web/pull/1324))
- Allow Super Admin login during maintenance ([#1333](https://github.com/ScilifelabDataCentre/dds_web/pull/1333))

## Sprint (2022-12-09 - 2022-12-23)

- Dependency: Bump `certifi` due to CVE-2022-23491 ([#1337](https://github.com/ScilifelabDataCentre/dds_web/pull/1337))
- Dependency: Bump `jwcrypto` due to CVE-2022-3102 ([#1339](https://github.com/ScilifelabDataCentre/dds_web/pull/1339))
- Cronjob: Get number of units and users for reporting ([#1324](https://github.com/ScilifelabDataCentre/dds_web/pull/1335))
- Add ability to change project information via ProjectInfo endpoint ([#1331](https://github.com/ScilifelabDataCentre/dds_web/pull/1331))
1 change: 0 additions & 1 deletion dds_web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ def prepare():

# Verify cli version compatible
if "api/v1" in flask.request.path:
block_if_maintenance()
verify_cli_version(version_cli=flask.request.headers.get("X-Cli-Version"))

# Get message of the day
Expand Down
81 changes: 80 additions & 1 deletion dds_web/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ def put(self):


class ProjectInfo(flask_restful.Resource):
"""Get information for a specific project."""
"""Display and change Project information."""

@auth.login_required
@logging_bind_request
Expand All @@ -973,7 +973,86 @@ def get(self):
"Size": project.size,
"Title": project.title,
"Description": project.description,
"PI": project.pi,
}

return_info = {"project_info": project_info}
return return_info

@auth.login_required(role=["Unit Admin", "Unit Personnel", "Project Owner", "Researcher"])
@logging_bind_request
@dbsession
@json_required
def put(self):
"""Update Project information."""
# Get project ID, project and verify access
project_id = dds_web.utils.get_required_item(obj=flask.request.args, req="project")
project = dds_web.utils.collect_project(project_id=project_id)
dds_web.utils.verify_project_access(project=project)

# get the now info items
json_input = flask.request.json
new_title = json_input.get("title")
new_description = json_input.get("description")
new_pi = json_input.get("pi")

# if new title,validate title
if new_title:
title_validator = marshmallow.validate.And(
marshmallow.validate.Length(min=1),
dds_web.utils.contains_disallowed_characters,
error={
"required": {"message": "Title is required."},
"null": {"message": "Title is required."},
},
)
try:
title_validator(new_title)
except marshmallow.ValidationError as err:
raise DDSArgumentError(str(err))

# if new description,validate description
if new_description:
description_validator = marshmallow.validate.And(
marshmallow.validate.Length(min=1),
dds_web.utils.contains_unicode_emojis,
error={
"required": {"message": "A project description is required."},
"null": {"message": "A project description is required."},
},
)
try:
description_validator(new_description)
except marshmallow.ValidationError as err:
raise DDSArgumentError(str(err))

# if new PI,validate email address
if new_pi:
pi_validator = marshmallow.validate.Email(error="The PI email is invalid")
try:
pi_validator(new_pi)
except marshmallow.ValidationError as err:
raise DDSArgumentError(str(err))

# current date for date_updated
curr_date = dds_web.utils.current_time()

# update the items
if new_title:
project.title = new_title
if new_description:
project.description = new_description
if new_pi:
project.pi = new_pi
project.date_updated = curr_date
db.session.commit()

# return_message = {}
return_message = {
"message": f"{project.public_id} info was successfully updated.",
"title": project.title,
"description": project.description,
"pi": project.pi,
}

return return_message
1 change: 1 addition & 0 deletions dds_web/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Config(object):
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_DEFAULT_SENDER = ("SciLifeLab DDS", "[email protected]")
MAIL_DDS = "[email protected]"

TOKEN_ENDPOINT_ACCESS_LIMIT = "10/hour"
RATELIMIT_STORAGE_URI = os.environ.get(
Expand Down
83 changes: 83 additions & 0 deletions dds_web/scheduled_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,86 @@ def quarterly_usage():
flask.current_app.logger.exception(err)
db.session.rollback()
raise


# @scheduler.task("interval", id="reporting", seconds=30, misfire_grace_time=1)
@scheduler.task("cron", id="reporting", day="1", hour=0, minute=1)
def reporting_units_and_users():
"""At the start of every month, get number of units and users."""
# Imports
import csv
import flask_mail
import flask_sqlalchemy
import pathlib
from dds_web import errors, utils
from dds_web.database.models import User, Unit

# Get current date
current_date: str = utils.timestamp(ts_format="%Y-%m-%d")

# Location of reporting file
reporting_file: pathlib.Path = pathlib.Path("doc/reporting/dds-reporting.csv")

# Error default
error: str = None

# App context required
with scheduler.app.app_context():
# Get email address
recipient: str = scheduler.app.config.get("MAIL_DDS")
default_subject: str = "DDS Unit / User report"
default_body: str = f"This email contains the DDS unit- and user statistics. The data was collected on: {current_date}."
error_subject: str = f"Error in {default_subject}"
error_body: str = "The cronjob 'reporting' experienced issues"

# Get units and count them
units: flask_sqlalchemy.BaseQuery = Unit.query
num_units: int = units.count()

# Count users
users: flask_sqlalchemy.BaseQuery = User.query
num_users_total: int = users.count() # All users
num_superadmins: int = users.filter_by(type="superadmin").count() # Super Admins
num_unit_users: int = users.filter_by(type="unituser").count() # Unit Admins / Personnel
num_researchers: int = users.filter_by(type="researchuser").count() # Researchers
num_users_excl_superadmins: int = num_users_total - num_superadmins

# Verify that sum is correct
if sum([num_superadmins, num_unit_users, num_researchers]) != num_users_total:
error: str = "Sum of number of users incorrect."
# Define csv file and verify that it exists
elif not reporting_file.exists():
error: str = "Could not find the csv file."

if error:
# Send email about error
file_error_msg: flask_mail.Message = flask_mail.Message(
subject=error_subject,
recipients=[recipient],
body=f"{error_body}: {error}",
)
utils.send_email_with_retry(msg=file_error_msg)
raise Exception(error)

# Add row with new info
with reporting_file.open(mode="a") as repfile:
writer = csv.writer(repfile)
writer.writerow(
[
current_date,
num_units,
num_researchers,
num_unit_users,
num_users_excl_superadmins,
]
)

# Create email
msg: flask_mail.Message = flask_mail.Message(
subject=default_subject,
recipients=[recipient],
body=default_body,
)
with reporting_file.open(mode="r") as file: # Attach file
msg.attach(filename=reporting_file.name, content_type="text/csv", data=file.read())
utils.send_email_with_retry(msg=msg) # Send
17 changes: 14 additions & 3 deletions dds_web/security/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@

# Own modules
from dds_web import basic_auth, auth, mail
from dds_web.errors import AuthenticationError, AccessDeniedError, InviteError, TokenMissingError
from dds_web.errors import (
AuthenticationError,
AccessDeniedError,
InviteError,
TokenMissingError,
)
from dds_web.database import models
import dds_web.utils

Expand Down Expand Up @@ -233,6 +238,9 @@ def verify_token(token):
if not user:
raise AccessDeniedError(message="Invalid token. Try reauthenticating.")

# Block all users but Super Admins during maintenance
dds_web.utils.block_if_maintenance(user=user)

if user.password_reset:
token_expired = claims.get("exp")
token_issued = datetime.datetime.fromtimestamp(token_expired) - MFA_EXPIRES_IN
Expand Down Expand Up @@ -361,7 +369,7 @@ def decrypt_token(token):
key = jwk.JWK.from_password(flask.current_app.config.get("SECRET_KEY"))
# Decrypt token
try:
decrypted_token = jwt.JWT(key=key, jwt=token)
decrypted_token = jwt.JWT(key=key, jwt=token, expected_type="JWE")
except ValueError as exc:
# "Token format unrecognized"
raise AuthenticationError(message="Invalid token") from exc
Expand All @@ -379,7 +387,7 @@ def verify_token_signature(token):

# Verify token
try:
jwttoken = jwt.JWT(key=key, jwt=token, algs=["HS256"])
jwttoken = jwt.JWT(key=key, jwt=token, algs=["HS256"], expected_type="JWS")
return json.loads(jwttoken.claims)
except jwt.JWTExpired as exc:
# jwt dependency uses a 60 seconds leeway to check exp
Expand All @@ -396,6 +404,9 @@ def verify_password(username, password):
user = models.User.query.get(username)

if user and user.is_active and user.verify_password(input_password=password):
# Block all users but Super Admins during maintenance
dds_web.utils.block_if_maintenance(user=user)

if not user.totp_enabled:
send_hotp_email(user)
return user
3 changes: 2 additions & 1 deletion dds_web/security/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def encrypted_jwt_token(
expires_in=expires_in,
additional_claims=additional_claims,
),
expected_type="JWE",
)
key = jwk.JWK.from_password(flask.current_app.config.get("SECRET_KEY"))
token.make_encrypted_token(key)
Expand Down Expand Up @@ -99,7 +100,7 @@ def __signed_jwt_token(
data["sen_con"] = sensitive_content

key = jwk.JWK.from_password(flask.current_app.config.get("SECRET_KEY"))
token = jwt.JWT(header={"alg": "HS256"}, claims=data, algs=["HS256"])
token = jwt.JWT(header={"alg": "HS256"}, claims=data, algs=["HS256"], expected_type="JWS")
token.make_signed_token(key)
return token.serialize()

Expand Down
Loading

0 comments on commit 49ca6e7

Please sign in to comment.