Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions server/mergin/auth/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ paths:
post:
tags:
- user
summary: Update profile of user in sesssion
description: Update profile of user in sesssion
summary: Update profile of user in session
description: Update profile of user in session
operationId: mergin.auth.controller.update_user_profile
requestBody:
description: Updated profile
Expand Down Expand Up @@ -101,6 +101,8 @@ paths:
$ref: "#/components/responses/BadStatusResp"
"401":
$ref: "#/components/responses/UnauthorizedError"
"403":
$ref: "#/components/responses/Forbidden"
/app/auth/refresh/csrf:
get:
summary: Get refreshed csrf token
Expand Down Expand Up @@ -426,6 +428,8 @@ paths:
description: OK
"400":
$ref: "#/components/responses/BadStatusResp"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFoundResp"
/app/auth/reset-password/{token}:
Expand Down Expand Up @@ -462,6 +466,8 @@ paths:
description: OK
"400":
$ref: "#/components/responses/BadStatusResp"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFoundResp"
/app/auth/confirm-email/{token}:
Expand Down Expand Up @@ -895,6 +901,8 @@ components:
example: my-workspace
role:
$ref: "#/components/schemas/WorkspaceRole"
can_edit_profile:
type: boolean
LoginResponse:
allOf:
- $ref: "#/components/schemas/UserDetail"
Expand Down
18 changes: 17 additions & 1 deletion server/mergin/auth/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@

from .commands import add_commands
from .config import Configuration
from .models import User, UserProfile
from .models import User

# signal for other versions to listen to
user_account_closed = signal("user_account_closed")
user_created = signal("user_created")

CANNOT_EDIT_PROFILE_MSG = "You cannot edit profile of this user"


def register(app):
"""Register mergin auth module in Flask app
Expand Down Expand Up @@ -70,6 +72,20 @@ def wrapped_func(*args, **kwargs):
return wrapped_func


def edit_profile_enabled(f):
"""Decorator to check if user can edit their profile (it is not allowed for SSO users)"""

@functools.wraps(f)
def wrapped_func(*args, **kwargs):
if not current_user or not current_user.is_authenticated:
return "Authentication information is missing or invalid.", 401
if not current_user.can_edit_profile:
return CANNOT_EDIT_PROFILE_MSG, 403
return f(*args, **kwargs)

return wrapped_func


def authenticate(login, password):
if "@" in login:
query = func.lower(User.email) == func.lower(login)
Expand Down
12 changes: 12 additions & 0 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
generate_confirmation_token,
user_created,
user_account_closed,
edit_profile_enabled,
CANNOT_EDIT_PROFILE_MSG,
)
from .bearer import encode_token
from .models import User, LoginHistory, UserProfile
Expand Down Expand Up @@ -254,6 +256,7 @@ def logout(): # pylint: disable=W0613,W0612


@auth_required
@edit_profile_enabled
def change_password(): # pylint: disable=W0613,W0612
form = UserChangePasswordForm()
if form.validate_on_submit():
Expand All @@ -268,6 +271,7 @@ def change_password(): # pylint: disable=W0613,W0612


@auth_required
@edit_profile_enabled
def resend_confirm_email(): # pylint: disable=W0613,W0612
send_confirmation_email(
current_app,
Expand All @@ -292,6 +296,9 @@ def password_reset(): # pylint: disable=W0613,W0612
if not user.active:
# user should confirm email first
return jsonify({"email": ["Account is not active"]}), 400
if not user.can_edit_profile:
# using SSO
abort(403, CANNOT_EDIT_PROFILE_MSG)

send_confirmation_email(
current_app,
Expand All @@ -311,6 +318,8 @@ def confirm_new_password(token): # pylint: disable=W0613,W0612
user = User.query.filter_by(email=email).first_or_404()
if not user.active:
abort(400, "Account is not active")
if not user.can_edit_profile:
abort(403, CANNOT_EDIT_PROFILE_MSG)

form = UserPasswordForm.from_json(request.json)
if form.validate():
Expand All @@ -331,6 +340,8 @@ def confirm_email(token): # pylint: disable=W0613,W0612
abort(400, "Invalid token")

user = User.query.filter_by(email=email).first_or_404()
if not user.can_edit_profile:
abort(403, CANNOT_EDIT_PROFILE_MSG)
if user.verified_email:
return "", 200

Expand All @@ -343,6 +354,7 @@ def confirm_email(token): # pylint: disable=W0613,W0612


@auth_required
@edit_profile_enabled
def update_user_profile(): # pylint: disable=W0613,W0612
form = UserProfileDataForm.from_json(request.json)
email_changed = current_user.email != form.email.data.strip()
Expand Down
14 changes: 12 additions & 2 deletions server/mergin/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class User(db.Model):
db.Index("ix_user_email", func.lower(email), unique=True),
)

def __init__(self, username, email, passwd, is_admin=False):
def __init__(self, username, email, passwd=None, is_admin=False):
self.username = username
self.email = email
self.assign_password(passwd)
Expand All @@ -58,7 +58,11 @@ def check_password(self, password):
def assign_password(self, password):
if isinstance(password, str):
password = password.encode("utf-8")
self.passwd = bcrypt.hashpw(password, bcrypt.gensalt()).decode("utf-8")
self.passwd = (
bcrypt.hashpw(password, bcrypt.gensalt()).decode("utf-8")
if password
else None
)

@property
def is_authenticated(self):
Expand Down Expand Up @@ -236,6 +240,12 @@ def create(
db.session.commit()
return user

@property
def can_edit_profile(self) -> bool:
"""Flag if we allow user to edit their email and name"""
# False when user is created by SSO login
return self.passwd is not None and self.active


class UserProfile(db.Model):
user_id = db.Column(
Expand Down
1 change: 1 addition & 0 deletions server/mergin/auth/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class UserInfoSchema(ma.SQLAlchemyAutoSchema):
receive_notifications = fields.Boolean(attribute="profile.receive_notifications")
registration_date = DateTimeWithZ(attribute="registration_date")
name = fields.Function(lambda obj: obj.profile.name())
can_edit_profile = fields.Boolean(attribute="can_edit_profile")

class Meta:
model = User
Expand Down
13 changes: 13 additions & 0 deletions server/mergin/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,19 @@ def test_update_user_profile(client):
assert not user.verified_email
assert user.email == "changed_email@mergin.co.uk"

# do not allow to update sso user
sso_user = add_user("sso_user", "sso")
login(client, sso_user.username, "sso")
sso_user.passwd = None
db.session.add(sso_user)
db.session.commit()
resp = client.post(
url_for("/.mergin_auth_controller_update_user_profile"),
data=json.dumps({"email": "changed_email@sso.co.uk"}),
headers=json_headers,
)
assert resp.status_code == 403


def test_search_user(client):
user = User.query.filter_by(username="mergin").first()
Expand Down
1 change: 1 addition & 0 deletions web-app/packages/lib/src/modules/user/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface UserDetailResponse extends UserProfileResponse {
receive_notifications: boolean
registration_date: string
workspaces: UserWorkspace[]
can_edit_profile: boolean
}

export interface WorkspaceResponse extends UserWorkspace {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
>
<!-- Title with buttons -->
<h1 class="headline-h3 text-color font-semibold">Account details</h1>
<div class="flex flex-grow-1 align-items-center lg:justify-content-end">
<div
v-if="loggedUser.can_edit_profile"
class="flex flex-grow-1 align-items-center lg:justify-content-end"
>
<PButton
@click="editProfileDialog"
icon="ti ti-pencil"
Expand Down
Loading