Skip to content

Commit

Permalink
Merge pull request #377 from MerginMaps/release-2025.2.0
Browse files Browse the repository at this point in the history
Release 2025.2.0
  • Loading branch information
MarcelGeo authored Feb 13, 2025
2 parents 566158a + bcb81cf commit 8105541
Show file tree
Hide file tree
Showing 40 changed files with 1,415 additions and 685 deletions.
6 changes: 6 additions & 0 deletions .prod.env
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ SECRET_KEY=fixme

#BEARER_TOKEN_EXPIRATION=3600 * 12 # in seconds

#SECURITY_BEARER_SALT=NODEFAULT
SECURITY_BEARER_SALT=fixme

#SECURITY_EMAIL_SALT=NODEFAULT
SECURITY_EMAIL_SALT=fixme

#SECURITY_PASSWORD_SALT=NODEFAULT
SECURITY_PASSWORD_SALT=fixme

Expand Down
3 changes: 3 additions & 0 deletions server/.test.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ GLOBAL_WORKSPACE='mergin'
GLOBAL_STORAGE=104857600
COLLECT_STATISTICS=0
GEODIFF_WORKING_DIR=/tmp/geodiff
SECURITY_BEARER_SALT='bearer'
SECURITY_EMAIL_SALT='email'
SECURITY_PASSWORD_SALT='password'
2 changes: 1 addition & 1 deletion server/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ urllib3 = "==2.2.2"
shapely = "==2.0.6"
psycogreen = "==1.0.2"
importlib-metadata = "==8.4.0" # https://github.com/pallets/flask/issues/4502
typing_extensions= "==4.12.2"
typing_extensions = "==4.12.2"
# requirements for development on windows
colorama = "==0.4.5"

Expand Down
965 changes: 492 additions & 473 deletions server/Pipfile.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from mergin.sync.tasks import remove_temp_files, remove_projects_backups
from mergin.celery import celery, configure_celery
from mergin.stats.config import Configuration
from mergin.stats.tasks import send_statistics
from mergin.stats.tasks import save_statistics, send_statistics
from mergin.stats.app import register as register_stats

Configuration.SERVER_TYPE = "ce"
Expand Down Expand Up @@ -65,14 +65,14 @@ def setup_periodic_tasks(sender, **kwargs):
remove_projects_backups,
name="remove old project backups",
)
sender.add_periodic_task(
crontab(hour="*/12", minute=0),
save_statistics,
name="Save usage statistics to database",
)
if Configuration.COLLECT_STATISTICS:
sender.add_periodic_task(
crontab(hour=randint(0, 5), minute=randint(0, 60)),
send_statistics,
name="send usage statistics",
)


# send report after start
if Configuration.COLLECT_STATISTICS:
send_statistics.delay()
2 changes: 2 additions & 0 deletions server/mergin/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
GEODIFF_LOGGER_LEVEL="2"
# only for dev - should be overwritten in production
SECRET_KEY='top-secret'
SECURITY_BEARER_SALT='top-secret'
SECURITY_EMAIL_SALT='top-secret'
SECURITY_PASSWORD_SALT='top-secret'
MAIL_DEFAULT_SENDER=''
FLASK_DEBUG=0
10 changes: 1 addition & 9 deletions server/mergin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,6 @@ def create_simple_app() -> Flask:
if Configuration.GEVENT_WORKER:
flask_app.wsgi_app = GeventTimeoutMiddleware(flask_app.wsgi_app)

@flask_app.cli.command()
def init_db():
"""Re-creates application database"""
print("Database initialization ...")
db.drop_all(bind=None)
db.create_all(bind=None)
db.session.commit()
print("Done. Tables created.")

add_commands(flask_app)

return flask_app
Expand Down Expand Up @@ -211,6 +202,7 @@ def load_user_from_header(header_val): # pylint: disable=W0613,W0612
try:
data = decode_token(
app.app.config["SECRET_KEY"],
app.app.config["SECURITY_BEARER_SALT"],
header_val,
app.app.config["BEARER_TOKEN_EXPIRATION"],
)
Expand Down
17 changes: 10 additions & 7 deletions server/mergin/auth/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,15 @@ def authenticate(login, password):
return user


def generate_confirmation_token(app, email):
def generate_confirmation_token(app, email, salt):
serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"])
return serializer.dumps(email, salt=app.config["SECURITY_PASSWORD_SALT"])
return serializer.dumps(email, salt=salt)


def confirm_token(token, expiration=3600 * 24 * 3):
def confirm_token(token, salt, expiration=3600):
serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
try:
email = serializer.loads(
token, salt=current_app.config["SECURITY_PASSWORD_SALT"], max_age=expiration
)
email = serializer.loads(token, salt=salt, max_age=expiration)
except:
return
return email
Expand All @@ -103,7 +101,12 @@ def send_confirmation_email(app, user, url, template, header, **kwargs):
"""
from ..celery import send_email_async

token = generate_confirmation_token(app, user.email)
salt = (
app.config["SECURITY_EMAIL_SALT"]
if url == "confirm-email"
else app.config["SECURITY_PASSWORD_SALT"]
)
token = generate_confirmation_token(app, user.email, salt)
confirm_url = f"{url}/{token}"
html = render_template(
template, subject=header, confirm_url=confirm_url, user=user, **kwargs
Expand Down
6 changes: 2 additions & 4 deletions server/mergin/auth/bearer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from flask.sessions import TaggedJSONSerializer


def decode_token(secret_key, token, max_age=None):
salt = "bearer-session"
def decode_token(secret_key, salt, token, max_age=None):
serializer = TaggedJSONSerializer()
signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1}
s = URLSafeTimedSerializer(
Expand All @@ -17,8 +16,7 @@ def decode_token(secret_key, token, max_age=None):
return s.loads(token, max_age=max_age)


def encode_token(secret_key, data):
salt = "bearer-session"
def encode_token(secret_key, salt, data):
serializer = TaggedJSONSerializer()
signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1}
s = URLSafeTimedSerializer(
Expand Down
2 changes: 2 additions & 0 deletions server/mergin/auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class Configuration(object):
SECURITY_BEARER_SALT = config("SECURITY_BEARER_SALT")
SECURITY_EMAIL_SALT = config("SECURITY_EMAIL_SALT")
SECURITY_PASSWORD_SALT = config("SECURITY_PASSWORD_SALT")
BEARER_TOKEN_EXPIRATION = config(
"BEARER_TOKEN_EXPIRATION", default=3600 * 12, cast=int
Expand Down
21 changes: 17 additions & 4 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
from ..sync.utils import files_size


EMAIL_CONFIRMATION_EXPIRATION = 12 * 3600


# public endpoints
def user_profile(user, return_all=True):
"""Return user profile in json format
Expand Down Expand Up @@ -143,7 +146,11 @@ def login_public(): # noqa: E501
"email": user.email,
"expire": str(expire),
}
token = encode_token(current_app.config["SECRET_KEY"], token_data)
token = encode_token(
current_app.config["SECRET_KEY"],
current_app.config["SECURITY_BEARER_SALT"],
token_data,
)

data = user_profile(user)
data["session"] = {"token": token, "expire": expire}
Expand Down Expand Up @@ -297,7 +304,7 @@ def password_reset(): # pylint: disable=W0613,W0612


def confirm_new_password(token): # pylint: disable=W0613,W0612
email = confirm_token(token)
email = confirm_token(token, salt=current_app.config["SECURITY_PASSWORD_SALT"])
if not email:
abort(400, "Invalid token")

Expand All @@ -315,7 +322,11 @@ def confirm_new_password(token): # pylint: disable=W0613,W0612


def confirm_email(token): # pylint: disable=W0613,W0612
email = confirm_token(token)
email = confirm_token(
token,
expiration=EMAIL_CONFIRMATION_EXPIRATION,
salt=current_app.config["SECURITY_EMAIL_SALT"],
)
if not email:
abort(400, "Invalid token")

Expand Down Expand Up @@ -375,7 +386,9 @@ def register_user(): # pylint: disable=W0613,W0612
if form.validate():
user = User.create(form.username.data, form.email.data, form.password.data)
user_created.send(user, source="admin")
token = generate_confirmation_token(current_app, user.email)
token = generate_confirmation_token(
current_app, user.email, current_app.config["SECURITY_EMAIL_SALT"]
)
confirm_url = f"confirm-email/{token}"
html = render_template(
"email/user_created.html",
Expand Down
25 changes: 19 additions & 6 deletions server/mergin/auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,26 @@


def username_validation(form, field):
from ..sync.utils import is_name_allowed
from ..sync.utils import (
has_valid_characters,
has_valid_first_character,
check_filename,
is_reserved_word,
)

if field.data and (not is_name_allowed(field.data) or "@" in field.data):
raise ValidationError(
f"Please don't start username with . and "
f"use only alphanumeric or these -._! characters in {field.name}."
)
errors = (
[
has_valid_characters(field.data),
has_valid_first_character(field.data),
is_reserved_word(field.data),
check_filename(field.data),
]
if field.data
else []
)
for error in errors:
if error:
raise ValidationError(error)


class PasswordValidator:
Expand Down
Loading

0 comments on commit 8105541

Please sign in to comment.