Skip to content

Commit

Permalink
Merge pull request #389 from kikkomep/conf/lsaai-settings
Browse files Browse the repository at this point in the history
feat: support for LSAAI v2
  • Loading branch information
kikkomep authored May 2, 2024
2 parents d3af054 + 3895724 commit 46e8b81
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 55 deletions.
3 changes: 3 additions & 0 deletions docker/lifemonitor.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ COPY --chown=lm:lm lifemonitor /lm/lifemonitor
COPY --chown=lm:lm migrations /lm/migrations
COPY --chown=lm:lm cli /lm/cli

# Ensure read access to source code to unprivileged users
RUN find /lm/lifemonitor/ -type d -exec chmod a+r {} \;

##################################################################
## Node Stage
##################################################################
Expand Down
11 changes: 9 additions & 2 deletions lifemonitor/auth/oauth2/client/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ def authorize(name):
user_info = remote.userinfo(token=token)
return _handle_authorize(remote, token, user_info)
except OAuthError as e:
logger.debug(e)
return e.description, 401
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise exceptions.OAuthAuthorizationException(title="Authorization Error",
detail=e.description)
except Exception as e:
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise exceptions.OAuthAuthorizationException(title="Authorization Error",
detail="Unable to authorize the user")

@blueprint.route('/login/<name>')
@next_route_aware
Expand Down
1 change: 1 addition & 0 deletions lifemonitor/auth/oauth2/client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def to_dict(self):

@staticmethod
def from_dict(data: dict):
assert data, "User data from the OAuth Provider cannot be empty"
profile = OAuthUserProfile()
for k, v, in data.items():
setattr(profile, k, v)
Expand Down
53 changes: 19 additions & 34 deletions lifemonitor/auth/oauth2/client/providers/lsaai.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,26 @@
import logging

from flask import current_app

from lifemonitor.exceptions import OAuthAuthorizationException
from lifemonitor.auth.oauth2.client.models import OAuthIdentity

# Config a module level logger
logger = logging.getLogger(__name__)


def normalize_userinfo(client, data):
logger.debug("LSAAI Data: %r", data)
preferred_username = data.get('eduperson_principal_name')[0].replace('@lifescience-ri.eu', '') \
if 'eduperson_principal_namex' in data and len(data['eduperson_principal_name']) > 0 \
else data['name'].replace(' ', '')
params = {
'sub': str(data['sub']),
'name': data['name'],
'email': data.get('email'),
'preferred_username': preferred_username
# 'profile': data['html_url'],
# 'picture': data['avatar_url'],
# 'website': data.get('blog'),
}

# The email can be be None despite the scope being 'user:email'.
# That is because a user can choose to make his/her email private.
# If that is the case we get all the users emails regardless if private or note
# and use the one he/she has marked as `primary`
info = {}
try:
if params.get('email') is None:
resp = client.get('user/emails')
resp.raise_for_status()
data = resp.json()
params["email"] = next(email['email'] for email in data if email['primary'])
except Exception as e:
logger.warning("Unable to get user email. Reason: %r", str(e))
return params
info["sub"] = data['sub']
except KeyError:
raise OAuthAuthorizationException(title="Unable to get user data",
description="the LS ID is required")
for key in ['name', 'email', 'preferred_username']:
info[key] = data.get(key, None)
if info[key] is None:
logger.warning("User %r has no %r", info["sub"], key)

return info


class LsAAI:
Expand All @@ -65,15 +50,15 @@ class LsAAI:
'client_id': current_app.config.get('LSAAI_CLIENT_ID', None),
'client_secret': current_app.config.get('LSAAI_CLIENT_SECRET', None),
'client_name': client_name,
'uri': 'https://proxy.aai.lifescience-ri.eu',
'api_base_url': 'https://proxy.aai.lifescience-ri.eu',
'access_token_url': 'https://proxy.aai.lifescience-ri.eu/OIDC/token',
'authorize_url': 'https://proxy.aai.lifescience-ri.eu/saml2sp/OIDC/authorization',
'client_kwargs': {'scope': 'openid profile email orcid eduperson_principal_name'},
'userinfo_endpoint': 'https://proxy.aai.lifescience-ri.eu/OIDC/userinfo',
'uri': 'https://login.aai.lifescience-ri.eu',
'api_base_url': 'https://login.aai.lifescience-ri.eu',
'access_token_url': 'https://login.aai.lifescience-ri.eu/oidc/token',
'authorize_url': 'https://login.aai.lifescience-ri.eu/oidc/authorize',
'client_kwargs': {'scope': 'openid profile email'},
'userinfo_endpoint': 'https://login.aai.lifescience-ri.eu/oidc/userinfo',
'userinfo_compliance_fix': normalize_userinfo,
'user_profile_html': 'https://profile.aai.lifescience-ri.eu/profile',
'server_metadata_url': 'https://proxy.aai.lifescience-ri.eu/.well-known/openid-configuration'
'server_metadata_url': 'https://login.aai.lifescience-ri.eu/oidc/.well-known/openid-configuration'
}

def __repr__(self) -> str:
Expand Down
18 changes: 11 additions & 7 deletions lifemonitor/auth/templates/auth/identity_not_found.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
{% block body %}

<div class="login-box" style="height: auto;">

{{ macros.render_logo(class="login-logo", style="width: auto") }}

<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">
<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">

<div class="card-body login-card-body">
{% if not identity %}
Expand All @@ -26,7 +26,11 @@
</div>
{{ macros.render_provider_logo(identity.provider) }}
<h5 class="login-box-msg text-bold mt-4" style="font-weight: lighter; font-size: 1.6em;">
{% if identity.user_info.name or identity.user_info.preferred_username %}
Hi, {{ identity.user_info.name or identity.user_info.preferred_username }}!
{% else %}
Hi, there!
{% endif %}
</h5>

<div class="text-center">
Expand All @@ -35,26 +39,26 @@
If you already have an account, we strongly recommend
that you <span class="text-center text-bold">Sign In</span>
with your existing credentials and link your new identity
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>.
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>.
</p>
<p class="text-bold">- OR -</p>
<p class="p-1 pb-4">
Click on <b>Register</b> to create a new account
linked to your <b>{{ identity.provider.name }}</b> identity.
</p>
</div>
</div>
</div>
{% endif %}

<form method="POST" action="{{action}}" >

{{ form.hidden_tag() }}

<div class="text-center mb-3 row">
<div class="text-center mb-3 row">
<div class="col-6">
<a href="{{ url_for("auth.login") }}" class="btn btn-block btn-secondary">
Sign In
</a>
</a>
</div>
<div class="col-6">
<a href="{{ url_for("auth.register_identity") }}" class="btn btn-block btn-primary">
Expand All @@ -69,4 +73,4 @@
</div>
</div>

{% endblock body %}
{% endblock body %}
26 changes: 15 additions & 11 deletions lifemonitor/auth/templates/auth/register.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
{% block body %}

<div class="login-box" style="height: auto;">

{{ macros.render_logo(class="login-logo", style="width: auto") }}

<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">
<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">

<div class="card-body login-card-body">
{% if not identity %}
<h5 class="login-box-msg text-bold m-0">Sign Up</h5>
{% else %}
<div class="text-center">
<div class="small text-muted m-2">
<div>
<div>
Sign Up for <span style="font-style: italic; font-family: Baskerville,Baskerville Old Face,Hoefler Text,Garamond,Times New Roman,serif;">Life</span><span class="small" style="font-size: 75%; margin: 0 -1px 0 1px;">-</span><span style="font-weight: bold; font-family: Gill Sans,Gill Sans MT,Calibri,sans-serif;">Monitor</span>
</div>
<div class="m-n1">
Expand All @@ -26,17 +26,21 @@
</div>
{{ macros.render_provider_logo(identity.provider) }}
<h5 class="login-box-msg text-bold mt-4" style="font-weight: lighter; font-size: 1.6em;">
{% if identity.user_info.name or identity.user_info.preferred_username %}
Hi, {{ identity.user_info.name or identity.user_info.preferred_username }}!
{% else %}
Hi, there!
{% endif %}
</h5>
</div>
</div>
<div class="mt-4 small text-muted" style="font-weight: lighter;">
Choose a username for your LifeMonitor account:
</div>
{% endif %}

<form method="POST" action="{{action}}" >
{% if identity %}
{{ form.identity(value=identity.user_info.sub) | safe }}
{{ form.identity(value=identity.user_info.sub) | safe }}
{% endif %}
{{ macros.render_custom_field(form.username, value=user.username if user else "") }}
{% if not identity %}
Expand All @@ -45,24 +49,24 @@
{% endif %}
{{ form.hidden_tag() }}

<div class="text-center mb-3 row">
<div class="text-center mb-3 row">
<div class="col-6">
<a href="{{ url_for("auth.login") }}" class="btn btn-block btn-secondary">
Back
</a>
</a>
</div>
<div class="col-6">
<button type="submit"
class="btn btn-block btn-primary">
Register
</button>
</button>
</div>
</div>

</form>

{% if not identity %}
<div class="social-auth-links text-center mb-3">
<div class="social-auth-links text-center mb-3">
<p class="text-bold">- OR -</p>
{% for p in providers %}
{% if p.client_name != 'lsaai' %}
Expand Down Expand Up @@ -92,12 +96,12 @@
Rather than creating a new account, we strongly recommend
that you <a href="{{ url_for("auth.login") }}" class="text-center">Sign In</a>
with your existing credentials and link your new identity
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>
</div>
</p>
{% endif %}
</div>
</div>
</div>

{% endblock body %}
{% endblock body %}
14 changes: 14 additions & 0 deletions lifemonitor/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ def handle_400(e: Exception = None, description: str = None):
)


@blueprint.route("/401")
def handle_401(e: Exception = None, description: str = None):
return __handle_error__(
{
"title": getattr(e, 'title', None) or "Unauthorized",
"code": "401",
"description": description if description
else str(e) if e and logger.isEnabledFor(logging.DEBUG)
else "Bad request",
}
)


@blueprint.route("/404")
def handle_404(e: Exception = None):
resource = request.args.get("resource", None, type=str)
Expand Down Expand Up @@ -187,6 +200,7 @@ def register_api(app):
logger.debug("Registering errors blueprint")
app.register_blueprint(blueprint)
app.register_error_handler(400, handle_400)
app.register_error_handler(401, handle_401)
app.register_error_handler(404, handle_404)
app.register_error_handler(429, handle_429)
app.register_error_handler(500, handle_500)
Expand Down
8 changes: 8 additions & 0 deletions lifemonitor/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ def __init__(self, detail=None,
detail=detail, status=status, **kwargs)


class OAuthAuthorizationException(LifeMonitorException):

def __init__(self, detail=None, title="OAuth Authorization Exception",
type="about:blank", status=401, **kwargs):
super().__init__(title=title,
detail=detail, status=status, **kwargs)


def handle_exception(e: Exception):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
Expand Down
2 changes: 1 addition & 1 deletion lifemonitor/templates/base.j2
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
<script>
initCookieConsentBanner('{{ domain }}')
</script>
{% endif %}
{% endif %}
</body>


0 comments on commit 46e8b81

Please sign in to comment.