ositah 24.7.dev1__py3-none-any.whl → 24.7.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ositah/__init__.py +0 -0
- ositah/app.py +17 -0
- ositah/apps/__init__.py +0 -0
- ositah/apps/analysis.py +774 -0
- ositah/apps/configuration/__init__.py +0 -0
- ositah/apps/configuration/callbacks.py +917 -0
- ositah/apps/configuration/main.py +546 -0
- ositah/apps/configuration/parameters.py +74 -0
- ositah/apps/configuration/tools.py +112 -0
- ositah/apps/export.py +1172 -0
- ositah/apps/validation/__init__.py +0 -0
- ositah/apps/validation/callbacks.py +240 -0
- ositah/apps/validation/main.py +89 -0
- ositah/apps/validation/parameters.py +25 -0
- ositah/apps/validation/tables.py +654 -0
- ositah/apps/validation/tools.py +533 -0
- ositah/assets/arrow_down_up.svg +4 -0
- ositah/assets/ositah.css +54 -0
- ositah/assets/sort_ascending.svg +5 -0
- ositah/assets/sort_descending.svg +6 -0
- ositah/assets/sorttable.js +499 -0
- ositah/main.py +449 -0
- ositah/ositah.example.cfg +215 -0
- ositah/static/style.css +54 -0
- ositah/templates/base.html +22 -0
- ositah/templates/bootstrap_login.html +38 -0
- ositah/templates/login_form.html +27 -0
- ositah/utils/__init__.py +0 -0
- ositah/utils/agents.py +117 -0
- ositah/utils/authentication.py +287 -0
- ositah/utils/cache.py +19 -0
- ositah/utils/core.py +13 -0
- ositah/utils/exceptions.py +64 -0
- ositah/utils/hito_db.py +51 -0
- ositah/utils/hito_db_model.py +253 -0
- ositah/utils/menus.py +339 -0
- ositah/utils/period.py +135 -0
- ositah/utils/projects.py +1175 -0
- ositah/utils/teams.py +42 -0
- ositah/utils/utils.py +458 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/METADATA +1 -1
- ositah-24.7.dev3.dist-info/RECORD +46 -0
- ositah-24.7.dev1.dist-info/RECORD +0 -6
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/LICENSE +0 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/WHEEL +0 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/entry_points.txt +0 -0
- {ositah-24.7.dev1.dist-info → ositah-24.7.dev3.dist-info}/top_level.txt +0 -0
ositah/static/style.css
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* CSS for the validation part of OSITAH */
|
|
2
|
+
|
|
3
|
+
div.login_page {
|
|
4
|
+
margin-left: 50px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
div.login_form_field {
|
|
8
|
+
font-weight: bold;
|
|
9
|
+
margin-top: 20px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
input.login_button {
|
|
13
|
+
margin-top: 30px;
|
|
14
|
+
margin-left: 45px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ul.flash-message {
|
|
18
|
+
display: inline-block;
|
|
19
|
+
list-style-type: none;
|
|
20
|
+
padding-left: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/* CSS for Dash components */
|
|
25
|
+
|
|
26
|
+
.team_list_dropdown {
|
|
27
|
+
margin-top: 15px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.validated_hito_missing {
|
|
31
|
+
background-color: tomato;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
table.sortable th::after, th.sorttable_sorted::after, th.sorttable_sorted_reverse::after {
|
|
35
|
+
content: " ";
|
|
36
|
+
display: inline-block;
|
|
37
|
+
width: 24px;
|
|
38
|
+
height: 24px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
|
|
42
|
+
content: url("arrow_down_up.svg");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
th.sorttable_sorted::after {
|
|
46
|
+
background: url("sort_ascending.svg");
|
|
47
|
+
background-size: contain;
|
|
48
|
+
}
|
|
49
|
+
th.sorttable_sorted_reverse::after {
|
|
50
|
+
background: url("sort_descending.svg");
|
|
51
|
+
background-size: cover;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#sorttable_sortfwdind, #sorttable_sortrevind { display: none; }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>OSITAH Login page</title>
|
|
6
|
+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
|
7
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
{% with messages = get_flashed_messages(with_categories=True) %}
|
|
11
|
+
{% if messages %}
|
|
12
|
+
<ul class="bg-warning flash-message">
|
|
13
|
+
{%- for category, message in messages %}
|
|
14
|
+
<li>{{ category }}: {{ message }}</li>
|
|
15
|
+
{% endfor -%}
|
|
16
|
+
</ul>
|
|
17
|
+
{% endif %}
|
|
18
|
+
{% endwith %}
|
|
19
|
+
|
|
20
|
+
{% block content %}{% endblock %}
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
6
|
+
<meta name="description" content="">
|
|
7
|
+
<meta name="author" content="">
|
|
8
|
+
<link rel="icon" href="/docs/4.0/assets/img/favicons/favicon.ico">
|
|
9
|
+
|
|
10
|
+
<title>Signin Template for Bootstrap</title>
|
|
11
|
+
|
|
12
|
+
<link rel="canonical" href="https://getbootstrap.com/docs/4.0/examples/sign-in/">
|
|
13
|
+
|
|
14
|
+
<!-- Bootstrap core CSS -->
|
|
15
|
+
<link href="../../dist/css/bootstrap.min.css" rel="stylesheet">
|
|
16
|
+
|
|
17
|
+
<!-- Custom styles for this template -->
|
|
18
|
+
<link href="signin.css" rel="stylesheet">
|
|
19
|
+
</head>
|
|
20
|
+
|
|
21
|
+
<body class="text-center">
|
|
22
|
+
<form class="form-signin">
|
|
23
|
+
<img class="mb-4" src="https://getbootstrap.com/docs/4.0/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72">
|
|
24
|
+
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
|
|
25
|
+
<label for="inputEmail" class="sr-only">Email address</label>
|
|
26
|
+
<input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
|
|
27
|
+
<label for="inputPassword" class="sr-only">Password</label>
|
|
28
|
+
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required>
|
|
29
|
+
<div class="checkbox mb-3">
|
|
30
|
+
<label>
|
|
31
|
+
<input type="checkbox" value="remember-me"> Remember me
|
|
32
|
+
</label>
|
|
33
|
+
</div>
|
|
34
|
+
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
|
35
|
+
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
|
|
36
|
+
</form>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div class="login_page">
|
|
2
|
+
{% extends 'base.html' %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<p>Log in using <strong>{{ provider.title }}</strong></p>
|
|
5
|
+
|
|
6
|
+
<form method=post>
|
|
7
|
+
<div class="login_form">
|
|
8
|
+
{% for field in form %}
|
|
9
|
+
{% if field.widget.__class__.__name__ == 'HiddenInput' %}
|
|
10
|
+
{{ field() }}
|
|
11
|
+
{% else %}
|
|
12
|
+
<div class="login_form_field">
|
|
13
|
+
{{ field.label() }}
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
{{ field() }}
|
|
17
|
+
{% if field.errors %}
|
|
18
|
+
Errors: {{ field.errors|join(' / ') }}
|
|
19
|
+
{% endif %}
|
|
20
|
+
</div>
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% endfor %}
|
|
23
|
+
<input type="submit" value="Login" class="btn btn-secondary login_button">
|
|
24
|
+
</div>
|
|
25
|
+
</form>
|
|
26
|
+
{% endblock %}
|
|
27
|
+
</div>
|
ositah/utils/__init__.py
ADDED
|
File without changes
|
ositah/utils/agents.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Helper functions to interact with the agent table
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from flask import session
|
|
4
|
+
|
|
5
|
+
from ositah.utils.exceptions import SessionDataMissing
|
|
6
|
+
from ositah.utils.utils import TEAM_LIST_ALL_AGENTS, GlobalParams, no_session_id_jumbotron
|
|
7
|
+
|
|
8
|
+
NSIP_AGENT_COLUMNS = {
|
|
9
|
+
"email_reseda": "email_reseda",
|
|
10
|
+
"firstname": "firstname",
|
|
11
|
+
"lastname": "lastname",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_agent_by_email(connexion_email: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Retrieve an agent from Hito using the connexion email. If the connexion email is not
|
|
18
|
+
defined for the agent, try to use the contact email.
|
|
19
|
+
|
|
20
|
+
:param connexion_email: agent's email
|
|
21
|
+
:return: agent entry (row)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ositah.utils.hito_db_model import Agent
|
|
25
|
+
|
|
26
|
+
user = Agent.query.filter_by(email_auth=session["user_email"]).first()
|
|
27
|
+
if user is None:
|
|
28
|
+
user = Agent.query.filter_by(email=session["user_email"]).first()
|
|
29
|
+
|
|
30
|
+
return user
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_agents(period_date: str, team: str = None) -> pd.DataFrame:
|
|
34
|
+
"""
|
|
35
|
+
Read agent table in Hito database and return it as a dataframe. If a cached version exists,
|
|
36
|
+
use it.
|
|
37
|
+
|
|
38
|
+
:param period_date: a date that must be inside the declaration period
|
|
39
|
+
:param team: selected team (and subteams). Ignored if None or TEAM_LIST_ALL_AGENTS.
|
|
40
|
+
:return: dataframe
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from ositah.utils.hito_db import get_db
|
|
44
|
+
|
|
45
|
+
global_params = GlobalParams()
|
|
46
|
+
columns = global_params.columns
|
|
47
|
+
db = get_db()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
session_data = global_params.session_data
|
|
51
|
+
except SessionDataMissing:
|
|
52
|
+
return no_session_id_jumbotron()
|
|
53
|
+
|
|
54
|
+
# start_date, _ = get_validation_period_dates(period_date)
|
|
55
|
+
|
|
56
|
+
agents = session_data.agent_list
|
|
57
|
+
if agents is None:
|
|
58
|
+
agent_list = pd.read_sql_query(global_params.agent_query, con=db.engine)
|
|
59
|
+
# WIP ; attempt to refine the agent list taking into account arrival and departure date
|
|
60
|
+
# Difficulty: carriere table has a suboptimal structure making a SQL request difficult
|
|
61
|
+
# TODO: query separately agent+team and carriere, then use Pandas to join them taking all
|
|
62
|
+
# the criteria into account
|
|
63
|
+
# Ignore archived agents that left before the start of the declaration period
|
|
64
|
+
# agent_list = agent_list.loc[(agent_list.archive == 0) |
|
|
65
|
+
# (agent_list.date_fin >= start_date.date().isoformat())]
|
|
66
|
+
if team and team != TEAM_LIST_ALL_AGENTS:
|
|
67
|
+
agent_list = agent_list[agent_list["team"].str.match(team, case=False, na=False)]
|
|
68
|
+
agent_list[columns["fullname"]] = agent_list[
|
|
69
|
+
[columns["lastname"], columns["firstname"]]
|
|
70
|
+
].agg(" ".join, axis=1)
|
|
71
|
+
agent_list.columns = agent_list.columns.str.lower()
|
|
72
|
+
agent_list["statut"] = agent_list["statut"].str.extract("^statut_(.+)$")
|
|
73
|
+
agent_list["optional"] = agent_list["statut"].isin(
|
|
74
|
+
global_params.declaration_options["optional_statutes"]
|
|
75
|
+
)
|
|
76
|
+
agent_list["email_auth"] = agent_list["email_auth"].str.lower()
|
|
77
|
+
team_list = pd.DataFrame(agent_list[columns["team"]].unique(), columns=["name"])
|
|
78
|
+
optional_teams_list = []
|
|
79
|
+
for opt_team in global_params.declaration_options["optional_teams"]:
|
|
80
|
+
optional_teams_list.append(
|
|
81
|
+
team_list[team_list["name"].str.match(opt_team, case=False, na=False)]["name"]
|
|
82
|
+
)
|
|
83
|
+
optional_teams = pd.concat(optional_teams_list)
|
|
84
|
+
agent_list.loc[~agent_list["optional"], "optional"] = agent_list["team"].isin(
|
|
85
|
+
optional_teams
|
|
86
|
+
)
|
|
87
|
+
session_data.agent_list = agent_list
|
|
88
|
+
else:
|
|
89
|
+
agent_list = session_data.agent_list
|
|
90
|
+
|
|
91
|
+
return agent_list
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_nsip_agents():
|
|
95
|
+
"""
|
|
96
|
+
Retrieve agents from NSIP and return them in a dataframe.
|
|
97
|
+
|
|
98
|
+
:return: dataframe or None if NSIP is not configured
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
global_params = GlobalParams()
|
|
102
|
+
|
|
103
|
+
if global_params.nsip:
|
|
104
|
+
agents = pd.DataFrame.from_dict(global_params.nsip.get_agent_list())
|
|
105
|
+
columns_to_delete = []
|
|
106
|
+
for c in agents.columns.tolist():
|
|
107
|
+
if c not in NSIP_AGENT_COLUMNS.values():
|
|
108
|
+
columns_to_delete.append(c)
|
|
109
|
+
agents["fullname"] = agents[
|
|
110
|
+
[NSIP_AGENT_COLUMNS["lastname"], NSIP_AGENT_COLUMNS["firstname"]]
|
|
111
|
+
].agg(" ".join, axis=1)
|
|
112
|
+
agents.drop(columns=columns_to_delete, inplace=True)
|
|
113
|
+
|
|
114
|
+
return agents
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
return None
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Module to handle user login with flask-multipass, a multi-backend authentication module for Flask
|
|
2
|
+
# The code is largely based on a simplified version of what Indico is doing and is focused on using
|
|
3
|
+
# LDAP (IJCLab ActiveDirectory) as the backend.
|
|
4
|
+
#
|
|
5
|
+
# There is no attempt to store session data in a database.
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import json
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
from uuid import uuid1
|
|
11
|
+
|
|
12
|
+
from flask import flash, redirect, request, session
|
|
13
|
+
from flask_multipass import InvalidCredentials, Multipass, NoSuchUser
|
|
14
|
+
|
|
15
|
+
from ositah.utils.utils import GlobalParams
|
|
16
|
+
|
|
17
|
+
# Redirect URL for login and logout
|
|
18
|
+
LOGIN_URL = "/login"
|
|
19
|
+
LOGOUT_URL = "/logout"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Session validity duration (in hours), i.e. max time since the last use
|
|
23
|
+
SESSION_MAX_DURATION = 4
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# List of authenticated users
|
|
27
|
+
user_list = {}
|
|
28
|
+
identity_list = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class User:
|
|
32
|
+
def __init__(self, first_name=None, last_name=None, email=None):
|
|
33
|
+
self._id = uuid1()
|
|
34
|
+
self._firstname = first_name
|
|
35
|
+
self._lastname = last_name
|
|
36
|
+
self._email = email
|
|
37
|
+
self._identities = []
|
|
38
|
+
|
|
39
|
+
def add_identity(self, identity):
|
|
40
|
+
self._identities.append(identity)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def identities(self):
|
|
44
|
+
return self._identities
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def email(self):
|
|
48
|
+
return self._email
|
|
49
|
+
|
|
50
|
+
def get_first_identity(self):
|
|
51
|
+
if len(self._identities) >= 1:
|
|
52
|
+
return self._identities[0]
|
|
53
|
+
else:
|
|
54
|
+
return Exception(f"No identity defined for user '{self.email}'")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def id(self):
|
|
58
|
+
return self._id
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Identity:
|
|
62
|
+
def __init__(self, provider=None, identifier=None):
|
|
63
|
+
self._id = identifier
|
|
64
|
+
self._provider = provider
|
|
65
|
+
self._multipass_data = None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def id(self):
|
|
69
|
+
return self._id
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def multipass_data(self):
|
|
73
|
+
return self._multipass_data
|
|
74
|
+
|
|
75
|
+
@multipass_data.setter
|
|
76
|
+
def multipass_data(self, data):
|
|
77
|
+
self._multipass_data = data
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def provider(self):
|
|
81
|
+
return self._provider
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OSITAHMultipass(Multipass):
|
|
85
|
+
def init_app(self, app):
|
|
86
|
+
super(OSITAHMultipass, self).init_app(app)
|
|
87
|
+
with app.app_context():
|
|
88
|
+
self._check_default_provider()
|
|
89
|
+
|
|
90
|
+
def _check_default_provider(self):
|
|
91
|
+
# Ensure that there is maximum one sync provider
|
|
92
|
+
sync_providers = [
|
|
93
|
+
p for p in self.identity_providers.values() if p.settings.get("synced_fields")
|
|
94
|
+
]
|
|
95
|
+
if len(sync_providers) > 1:
|
|
96
|
+
raise ValueError("There can only be one sync provider.")
|
|
97
|
+
# Ensure that there is exactly one form-based default auth provider
|
|
98
|
+
auth_providers = list(self.auth_providers.values())
|
|
99
|
+
external_providers = [p for p in auth_providers if p.is_external]
|
|
100
|
+
local_providers = [p for p in auth_providers if not p.is_external]
|
|
101
|
+
if any(p.settings.get("default") for p in external_providers):
|
|
102
|
+
raise ValueError("The default provider cannot be external")
|
|
103
|
+
if all(p.is_external for p in auth_providers):
|
|
104
|
+
return
|
|
105
|
+
default_providers = [p for p in auth_providers if p.settings.get("default")]
|
|
106
|
+
if len(default_providers) > 1:
|
|
107
|
+
raise ValueError("There can only be one default auth provider")
|
|
108
|
+
elif not default_providers:
|
|
109
|
+
if len(local_providers) == 1:
|
|
110
|
+
local_providers[0].settings["default"] = True
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("There is no default auth provider")
|
|
113
|
+
|
|
114
|
+
def handle_auth_error(self, exc, redirect_to_login=False):
|
|
115
|
+
if isinstance(exc, (NoSuchUser, InvalidCredentials)):
|
|
116
|
+
print("Invalid credentials")
|
|
117
|
+
else:
|
|
118
|
+
exc_str = str(exc)
|
|
119
|
+
print(
|
|
120
|
+
"Authentication via %s failed: %s (%r)",
|
|
121
|
+
exc.provider.name if exc.provider else None,
|
|
122
|
+
exc_str,
|
|
123
|
+
exc.details,
|
|
124
|
+
)
|
|
125
|
+
return super(OSITAHMultipass, self).handle_auth_error(
|
|
126
|
+
exc, redirect_to_login=redirect_to_login
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def configure_multipass_ldap(app, provider_title):
|
|
131
|
+
"""
|
|
132
|
+
Configure Flask_multipass from configuration. Inspired from Indico and its configuration file.
|
|
133
|
+
Required flask_multipass with PR #42.
|
|
134
|
+
|
|
135
|
+
:param app: Flask app
|
|
136
|
+
:param provider_title: text associated with the auth/identity provider
|
|
137
|
+
:return: none
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
global_params = GlobalParams()
|
|
141
|
+
config = global_params.ldap
|
|
142
|
+
|
|
143
|
+
if not config or len(config) == 0:
|
|
144
|
+
raise Exception("Missing LDAP configuration")
|
|
145
|
+
|
|
146
|
+
ldap_config = {
|
|
147
|
+
"uri": config["uri"],
|
|
148
|
+
"bind_dn": config["bind_dn"],
|
|
149
|
+
"bind_password": config["password"],
|
|
150
|
+
"timeout": 30,
|
|
151
|
+
"verify_cert": True,
|
|
152
|
+
"page_size": 10000,
|
|
153
|
+
"uid": "sAMAccountName",
|
|
154
|
+
"user_base": config["base_dn"],
|
|
155
|
+
"user_filter": "(objectcategory=user)",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
auth_provider = {
|
|
159
|
+
"ldap": {
|
|
160
|
+
"type": "ldap",
|
|
161
|
+
"title": provider_title,
|
|
162
|
+
"ldap": ldap_config,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
identity_provider = {
|
|
167
|
+
"ldap": {
|
|
168
|
+
"type": "ldap",
|
|
169
|
+
"title": provider_title,
|
|
170
|
+
"ldap": ldap_config,
|
|
171
|
+
"identifier_field": "mail",
|
|
172
|
+
"accepted_users": "all",
|
|
173
|
+
"mapping": {
|
|
174
|
+
"first_name": "givenName",
|
|
175
|
+
"last_name": "sn",
|
|
176
|
+
"email": "mail",
|
|
177
|
+
"affiliation": "company",
|
|
178
|
+
"phone": "telephoneNumber",
|
|
179
|
+
},
|
|
180
|
+
"trusted_email": True,
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
app.config["MULTIPASS_AUTH_PROVIDERS"] = auth_provider
|
|
185
|
+
app.config["MULTIPASS_IDENTITY_PROVIDERS"] = identity_provider
|
|
186
|
+
app.config["MULTIPASS_PROVIDER_MAP"] = {"ldap": "ldap"}
|
|
187
|
+
app.config["MULTIPASS_IDENTITY_INFO_KEYS"] = {"first_name", "last_name", "email"}
|
|
188
|
+
app.config["MULTIPASS_LOGIN_FORM_TEMPLATE"] = "login_form.html"
|
|
189
|
+
app.config["MULTIPASS_SUCCESS_ENDPOINT"] = "/"
|
|
190
|
+
app.config["MULTIPASS_FAILURE_MESSAGE"] = "Login failed: {error}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
multipass = OSITAHMultipass()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@multipass.identity_handler
|
|
197
|
+
def identity_handler(identity_info):
|
|
198
|
+
if identity_info.identifier in identity_list:
|
|
199
|
+
user = identity_list[identity_info.identifier]
|
|
200
|
+
identity = user.get_first_identity()
|
|
201
|
+
else:
|
|
202
|
+
if identity_info.data["email"] in user_list:
|
|
203
|
+
user = user_list[identity_info.data["email"]]
|
|
204
|
+
else:
|
|
205
|
+
user = User(**identity_info.data.to_dict())
|
|
206
|
+
user_list[user.email] = user
|
|
207
|
+
identity = Identity(
|
|
208
|
+
provider=identity_info.provider.name, identifier=identity_info.identifier
|
|
209
|
+
)
|
|
210
|
+
user.add_identity(identity)
|
|
211
|
+
identity_list[identity.id] = user
|
|
212
|
+
identity.multipass_data = json.dumps(identity_info.multipass_data)
|
|
213
|
+
session["user_id"] = identity.id
|
|
214
|
+
flash("Received IdentityInfo: {}".format(identity_info), "success")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def login_required(view):
|
|
218
|
+
"""
|
|
219
|
+
A decorator to require login on Flask views
|
|
220
|
+
|
|
221
|
+
:param view: a function
|
|
222
|
+
:return: decorated function
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
@functools.wraps(view)
|
|
226
|
+
def wrapped_view(**kwargs):
|
|
227
|
+
redirect_path = urlparse(request.base_url).path
|
|
228
|
+
if len(redirect_path) == 0:
|
|
229
|
+
redirect_path = "/"
|
|
230
|
+
if "user_id" not in session:
|
|
231
|
+
if redirect_path != "/favicon.ico":
|
|
232
|
+
return redirect(f"{LOGIN_URL}?next={redirect_path}")
|
|
233
|
+
elif redirect_path == LOGOUT_URL:
|
|
234
|
+
remove_session()
|
|
235
|
+
return multipass.logout("/", clear_session=True)
|
|
236
|
+
|
|
237
|
+
return view(**kwargs)
|
|
238
|
+
|
|
239
|
+
return wrapped_view
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def protect_views(app):
|
|
243
|
+
for view_func in app.server.view_functions:
|
|
244
|
+
if view_func.startswith("/<path:path>"):
|
|
245
|
+
app.server.view_functions[view_func] = login_required(
|
|
246
|
+
app.server.view_functions[view_func]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return app
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def remove_session():
|
|
253
|
+
"""
|
|
254
|
+
Remove a session from the database and do additional session cleanup.
|
|
255
|
+
|
|
256
|
+
:return: None
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
from sqlalchemy import delete
|
|
260
|
+
|
|
261
|
+
from ositah.utils.hito_db import get_db
|
|
262
|
+
from ositah.utils.hito_db_model import OSITAHSession
|
|
263
|
+
|
|
264
|
+
global_params = GlobalParams()
|
|
265
|
+
|
|
266
|
+
if "uid" in session:
|
|
267
|
+
del global_params.session_data
|
|
268
|
+
|
|
269
|
+
if session["user_id"] in identity_list:
|
|
270
|
+
del user_list[identity_list[session["user_id"]].email]
|
|
271
|
+
del identity_list[session["user_id"]]
|
|
272
|
+
else:
|
|
273
|
+
print(
|
|
274
|
+
(
|
|
275
|
+
f"WARNING: attempt to delete a non-existing user/identity"
|
|
276
|
+
f" {session['uid']} (user={session['user_id']})"
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if "user_email" in session:
|
|
281
|
+
sql_cmd = delete(OSITAHSession).where(
|
|
282
|
+
OSITAHSession.id == session["uid"],
|
|
283
|
+
OSITAHSession.email == session["user_email"],
|
|
284
|
+
)
|
|
285
|
+
db = get_db()
|
|
286
|
+
db.session.execute(sql_cmd)
|
|
287
|
+
db.session.commit()
|
ositah/utils/cache.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Helper functions to manage the data cache
|
|
2
|
+
|
|
3
|
+
from ositah.utils.exceptions import SessionDataMissing
|
|
4
|
+
from ositah.utils.utils import GlobalParams, no_session_id_jumbotron
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def clear_cached_data():
|
|
8
|
+
"""
|
|
9
|
+
Clear the data cached by the previous requests
|
|
10
|
+
|
|
11
|
+
:return: None
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
global_params = GlobalParams()
|
|
15
|
+
try:
|
|
16
|
+
session_data = global_params.session_data
|
|
17
|
+
session_data.reset_caches()
|
|
18
|
+
except SessionDataMissing:
|
|
19
|
+
return no_session_id_jumbotron()
|
ositah/utils/core.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Module with utility functions
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Singleton decorator definition
|
|
5
|
+
def singleton(cls):
|
|
6
|
+
instances = {}
|
|
7
|
+
|
|
8
|
+
def getinstance(*args, **kwargs):
|
|
9
|
+
if cls not in instances:
|
|
10
|
+
instances[cls] = cls(*args, **kwargs)
|
|
11
|
+
return instances[cls]
|
|
12
|
+
|
|
13
|
+
return getinstance
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# OSITAH exceptions not specific to one of the sub-application
|
|
2
|
+
|
|
3
|
+
from hito_tools.exceptions import EXIT_STATUS_GENERAL_ERROR
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InvalidCallbackInput(Exception):
|
|
7
|
+
def __init__(self, input_name):
|
|
8
|
+
self.msg = f"internal error: invalid input ({input_name}) in callback"
|
|
9
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
return repr(self.msg)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InvalidDataSource(Exception):
|
|
16
|
+
def __init__(self, source):
|
|
17
|
+
self.msg = f"attempt to use and invalid data source ({source})"
|
|
18
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
19
|
+
|
|
20
|
+
def __str__(self):
|
|
21
|
+
return repr(self.msg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidHitoProjectName(Exception):
|
|
25
|
+
def __init__(self, projects):
|
|
26
|
+
self.msg = (
|
|
27
|
+
f"The following Hito project names don't match the format 'masterproject / project' :"
|
|
28
|
+
f" {', '.join(projects)}"
|
|
29
|
+
)
|
|
30
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
31
|
+
|
|
32
|
+
def __str__(self):
|
|
33
|
+
return repr(self.msg)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SessionDataMissing(Exception):
|
|
37
|
+
def __init__(self, session_id=None):
|
|
38
|
+
if session_id:
|
|
39
|
+
session_txt = f" (session={session_id})"
|
|
40
|
+
else:
|
|
41
|
+
session_txt = ""
|
|
42
|
+
self.msg = f"Attempt to use non existing session data{session_txt}"
|
|
43
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
return repr(self.msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidationPeriodAmbiguous(Exception):
|
|
50
|
+
def __init__(self, date):
|
|
51
|
+
self.msg = f"Configuration error: several periods matching {date}"
|
|
52
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
53
|
+
|
|
54
|
+
def __str__(self):
|
|
55
|
+
return repr(self.msg)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ValidationPeriodMissing(Exception):
|
|
59
|
+
def __init__(self, date):
|
|
60
|
+
self.msg = f"No defined declaration period matching {date}"
|
|
61
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
62
|
+
|
|
63
|
+
def __str__(self):
|
|
64
|
+
return repr(self.msg)
|