Commit 9332a554747a59bbb348f0f82bad0a75ca89bd7f
Exists in
master
and in
4 other branches
File logging and route access control
Showing
25 changed files
with
385 additions
and
42 deletions
Show diff stats
.gitignore
CHANGELOG.md
... | ... | @@ -24,6 +24,13 @@ or major refactoring improvments. |
24 | 24 | |
25 | 25 | ## Unreleased |
26 | 26 | |
27 | +## [0.2.pre-6] - 2021-03-29 - Role Controle, and per site configuration | |
28 | +### New | |
29 | +Route access control by User role | |
30 | +Per site configuration | |
31 | +Route error handling | |
32 | +Logging to log file | |
33 | + | |
27 | 34 | ## [0.2.pre-5] - 2021-03-19 - More Deploy facilities |
28 | 35 | ### Changed |
29 | 36 | More documentation | ... | ... |
INSTALL.md
... | ... | @@ -22,6 +22,8 @@ |
22 | 22 | ### Configurer l'application |
23 | 23 | |
24 | 24 | # D'abord les accés base de donnée |
25 | + cp resources/pdc_config.py . | |
26 | + $(EDITOR) pdc_config.py | |
25 | 27 | cp resources/db_config.py . |
26 | 28 | $(EDITOR) db_config.py |
27 | 29 | |
... | ... | @@ -64,6 +66,7 @@ Utiliser l'outil de ligne de commande fourni avec l'application. |
64 | 66 | Les fichiers concernés: |
65 | 67 | |
66 | 68 | - pdc_web.wsgi |
69 | +- pdc_config.py | |
67 | 70 | - db_config.py |
68 | 71 | |
69 | 72 | La procédure: |
... | ... | @@ -82,7 +85,7 @@ La procédure: |
82 | 85 | python -m venv venv |
83 | 86 | source venv/bin/activate |
84 | 87 | pip install -r requirements.txt |
85 | - $(EDITOR) db_config.py .flaskenv | |
88 | + $(EDITOR) pdc_config.py db_config.py .flaskenv | |
86 | 89 | |
87 | 90 | # configurer le serveur web |
88 | 91 | cp ./resources/apache2-virtual-host.conf /etc/apache2/sites-available/pdc-web.conf |
... | ... | @@ -141,5 +144,3 @@ Normalement, cette configuration permet de lire les variables positionées dans |
141 | 144 | |
142 | 145 | |
143 | 146 | Ainsi fait, exécutez votre projet depuis pycharm et testez. |
144 | -flask db update | |
145 | -db_config | ... | ... |
VERSION.txt
app/__init__.py
... | ... | @@ -4,7 +4,18 @@ import sys |
4 | 4 | from flask import Flask |
5 | 5 | from flask_login import LoginManager |
6 | 6 | |
7 | -from app.models import db, User | |
7 | +import logging | |
8 | +from logging.handlers import RotatingFileHandler | |
9 | +from flask.logging import default_handler | |
10 | + | |
11 | +from app.models import db | |
12 | +from app.auth.models import User | |
13 | + | |
14 | +from dotenv import load_dotenv | |
15 | + | |
16 | +app_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') | |
17 | +env_file = os.path.join(app_dir, ".flaskenv") | |
18 | +load_dotenv(env_file, verbose=True) | |
8 | 19 | |
9 | 20 | login_manager = LoginManager() |
10 | 21 | |
... | ... | @@ -17,12 +28,13 @@ def load_user(user_id): |
17 | 28 | return User.query.get(int(user_id)) |
18 | 29 | |
19 | 30 | |
31 | +# TODO: move the following into create_app() | |
32 | +# and use app.logger system | |
20 | 33 | # Please, set a config file on top project dir |
21 | 34 | # see in ../pdc_config.py |
22 | 35 | try: |
23 | 36 | from pdc_config import Config, ProdConfig, DevConfig, TestConfig |
24 | 37 | except ImportError: |
25 | - # TODO: use logging system | |
26 | 38 | print("Please set a pdc_config.py file in you PYTHON_PATH") |
27 | 39 | print("See INSTALL.md for more info") |
28 | 40 | sys.exit(-1) |
... | ... | @@ -56,6 +68,25 @@ def create_app(config_class=None): |
56 | 68 | app = Flask(__name__) |
57 | 69 | app.config.from_object(config_class) |
58 | 70 | |
71 | + # Log to stdout or to file | |
72 | + # | |
73 | + app.logger.removeHandler(default_handler) | |
74 | + if app.debug or app.testing or app.config['LOG_TO_STDOUT']: | |
75 | + log_handler = logging.StreamHandler() | |
76 | + else: | |
77 | + logs_file = app.config['PDC_LOGS_FILE'] | |
78 | + logs_dir = os.path.dirname(logs_file) | |
79 | + if not os.path.exists(logs_dir): | |
80 | + os.mkdir(logs_dir) | |
81 | + log_handler = RotatingFileHandler(logs_file, maxBytes=10240, backupCount=10) | |
82 | + | |
83 | + log_handler.setFormatter(logging.Formatter( | |
84 | + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) | |
85 | + app.logger.addHandler(log_handler) | |
86 | + | |
87 | + app.logger.setLevel(app.config['PDC_LOGS_LEVEL']) | |
88 | + app.logger.info("Starting PDC-WEB") | |
89 | + | |
59 | 90 | db.init_app(app) |
60 | 91 | login_manager.init_app(app) |
61 | 92 | |
... | ... | @@ -63,7 +94,11 @@ def create_app(config_class=None): |
63 | 94 | # |
64 | 95 | from .main import bp as main_bp |
65 | 96 | app.register_blueprint(main_bp) |
97 | + | |
66 | 98 | from .auth import bp as auth_bp |
67 | 99 | app.register_blueprint(auth_bp) |
68 | 100 | |
101 | + from .errors import bp as errors_bp | |
102 | + app.register_blueprint(errors_bp) | |
103 | + | |
69 | 104 | return app | ... | ... |
... | ... | @@ -0,0 +1,70 @@ |
1 | +from pprint import pprint | |
2 | + | |
3 | +from flask_login import UserMixin, current_user | |
4 | +from app.models import db | |
5 | + | |
6 | +# | |
7 | +# Roles | |
8 | +# | |
9 | + | |
10 | +ADMIN = 50 | |
11 | +PROJECT = 40 | |
12 | +SERVICE = 30 | |
13 | +AGENT = 10 | |
14 | +PUBLIC = 0 | |
15 | + | |
16 | +_roleToName = { | |
17 | + ADMIN: 'ADMIN', | |
18 | + PROJECT: 'PROJECT', | |
19 | + SERVICE: 'SERVICE', | |
20 | + AGENT: 'AGENT', | |
21 | + PUBLIC: 'PUBLIC', | |
22 | +} | |
23 | +_nameToRole = { | |
24 | + 'ADMIN': ADMIN, | |
25 | + 'PROJECT': PROJECT, | |
26 | + 'SERVICE': SERVICE, | |
27 | + 'AGENT': AGENT, | |
28 | + 'PUBLIC': PUBLIC, | |
29 | +} | |
30 | + | |
31 | + | |
32 | +def _checkRole(role): | |
33 | + if isinstance(role, int): | |
34 | + rv = role | |
35 | + elif str(role) == role: | |
36 | + role = role.upper() | |
37 | + if role not in _nameToRole: | |
38 | + raise ValueError("Unknown role: %r" % role) | |
39 | + rv = _nameToRole[role] | |
40 | + else: | |
41 | + raise TypeError("Role not an integer or a valid string: %r" % role) | |
42 | + return rv | |
43 | + | |
44 | + | |
45 | +class User(UserMixin, db.Model): | |
46 | + id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy | |
47 | + email = db.Column(db.String(100), unique=True) | |
48 | + name = db.Column(db.String(100)) | |
49 | + login = db.Column(db.String(100), unique=True) | |
50 | + password = db.Column(db.String(100)) | |
51 | + role = db.Column(db.Integer, default=0) | |
52 | + | |
53 | + def __repr__(self): | |
54 | + return "i: {}, n: {}, e: {}, l: {}".format(self.id, self.name, self.email, self.login) | |
55 | + | |
56 | + # Set role at construction time | |
57 | + def __init__(self, **kwargs): | |
58 | + super(User, self).__init__(**kwargs) | |
59 | + self.set_role(kwargs['role']) | |
60 | + | |
61 | + def set_role(self, role): | |
62 | + self.role = _checkRole(role) | |
63 | + | |
64 | + def has_role(self, role): | |
65 | + role = _checkRole(role) | |
66 | + return self.role == role | |
67 | + | |
68 | + def has_role_or_higher(self, role): | |
69 | + role = _checkRole(role) | |
70 | + return self.role and (self.role >= role) | ... | ... |
app/auth/routes.py
1 | -from flask import render_template, request, redirect, url_for, flash | |
1 | +from functools import wraps | |
2 | + | |
3 | +from flask_login import current_user | |
4 | +from flask import render_template, request, redirect, url_for, flash, current_app | |
2 | 5 | from flask_login import login_user, logout_user |
3 | -from app.models import User | |
4 | 6 | |
5 | -from . import bp | |
7 | +from app.auth.models import User | |
8 | +from app.auth import bp | |
9 | + | |
10 | + | |
11 | +# | |
12 | +# Decorator used to protect routes by role | |
13 | +# inspired from https://flask.palletsprojects.com/en/master/patterns/viewdecorators/ | |
14 | +# | |
15 | +def role_required(role): | |
16 | + def decorator(f): | |
17 | + @wraps(f) | |
18 | + def decorated_function(*args, **kwargs): | |
19 | + try: | |
20 | + if current_app.config['ROLE_DISABLED']: | |
21 | + return f(*args, **kwargs) | |
22 | + except KeyError: | |
23 | + # no such config, juste ignore | |
24 | + pass | |
25 | + # first check use is logged in | |
26 | + if not current_user or not current_user.is_authenticated: | |
27 | + flash(f"Vous devez vous authentifier avec la fonction '{role}'", 'warning') | |
28 | + return redirect(url_for('auth.login')) | |
29 | + # then check role status | |
30 | + try: | |
31 | + is_authorised = current_user.has_role_or_higher(role) | |
32 | + except ValueError: | |
33 | + raise Exception("Unknowk role provided %s" % role) | |
34 | + if not is_authorised: | |
35 | + flash("Vous n'avez pas les autorisations pour accéder à cette page", 'dark') | |
36 | + return redirect(url_for('main.index')) | |
37 | + return f(*args, **kwargs) | |
38 | + | |
39 | + return decorated_function | |
40 | + | |
41 | + return decorator | |
6 | 42 | |
7 | 43 | |
8 | 44 | @bp.route('/login') | ... | ... |
app/auth/templates/login.html
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 | <html lang="fr"> |
3 | 3 | <head> |
4 | 4 | {% include 'heads.html' %} |
5 | + <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" type="text/css"/> | |
5 | 6 | <link href="{{ url_for('auth.static', filename='signin.css') }}" rel="stylesheet" type="text/css"/> |
6 | 7 | </head> |
7 | 8 | <body class="text-center"> |
... | ... | @@ -9,7 +10,7 @@ |
9 | 10 | {% include 'flash-messages.html' %} |
10 | 11 | |
11 | 12 | <a href="{{url_for('main.index')}}"> |
12 | - <img class="mb-4" height="72" src="{{ url_for('static', filename='img/pdc.png') }}" width="72"> | |
13 | + <img class="mb-4 {{config.PDC_SITE_CLASS}}" height="72" src="{{ url_for('static', filename='img/pdc.png') }}" width="72"> | |
13 | 14 | </a> |
14 | 15 | <h1 class="h3 font-weight-normal mb-3">Connectez vous</h1> |
15 | 16 | <label class="sr-only" for="login">Email Adress</label> | ... | ... |
app/commands/commands.py
... | ... | @@ -2,15 +2,15 @@ import sys |
2 | 2 | import click |
3 | 3 | import random |
4 | 4 | |
5 | +from flask import current_app | |
5 | 6 | from sqlalchemy.exc import OperationalError |
6 | 7 | from sqlalchemy.sql import func |
7 | 8 | from sqlalchemy.ext.automap import automap_base |
8 | 9 | from sqlalchemy.orm import Session |
9 | 10 | from sqlalchemy import create_engine |
10 | 11 | |
11 | -from app.models import db, User, Agent, Service, Project, Capacity, Period, Charge | |
12 | - | |
13 | -from db_config import mysql_lesia_uri | |
12 | +from app.models import db, Agent, Service, Project, Capacity, Period, Charge | |
13 | +from app.auth.models import User | |
14 | 14 | |
15 | 15 | from . import bp |
16 | 16 | |
... | ... | @@ -23,7 +23,7 @@ def feed_from_lesia(): |
23 | 23 | """ |
24 | 24 | Base = automap_base() |
25 | 25 | |
26 | - engine = create_engine(mysql_lesia_uri) | |
26 | + engine = create_engine(current_app.config['LESIA_AGENTS_DB_URI']) | |
27 | 27 | |
28 | 28 | # reflect the tables |
29 | 29 | try: |
... | ... | @@ -33,7 +33,6 @@ def feed_from_lesia(): |
33 | 33 | print("Please, configure the mysql database (see db_config.py)") |
34 | 34 | sys.exit(-1) |
35 | 35 | |
36 | - | |
37 | 36 | # mapped classes are now created with names by default |
38 | 37 | # matching that of the table name. |
39 | 38 | LesiaAgent = Base.classes.agent |
... | ... | @@ -97,7 +96,7 @@ def feed_random_charges(agent): |
97 | 96 | .filter(Charge.agent_id == agent_id, |
98 | 97 | Charge.period_id == period_id |
99 | 98 | ).scalar() |
100 | - if total_charge is not None and (total_charge + percent)>= 100: | |
99 | + if total_charge is not None and (total_charge + percent) >= 100: | |
101 | 100 | print("Skipping agent {} for period {}".format(agent_id, period_id)) |
102 | 101 | continue |
103 | 102 | charge = Charge(agent_id=agent_id, |
... | ... | @@ -124,7 +123,7 @@ def user_delete(user_id): |
124 | 123 | def create_db(): |
125 | 124 | """ Create the database structure.""" |
126 | 125 | db.create_all() |
127 | - admin = User(email='admin@nowhere.org', name='admin', login='admin', password='admin') | |
126 | + admin = User(email='admin@nowhere.org', name='admin', login='admin', password='admin', role='admin') | |
128 | 127 | db.session.add(admin) |
129 | 128 | db.session.commit() |
130 | 129 | ... | ... |
... | ... | @@ -0,0 +1,31 @@ |
1 | +from flask import render_template | |
2 | +from app import db | |
3 | +from . import bp | |
4 | + | |
5 | + | |
6 | +# Inspired by: | |
7 | +# https://flask.palletsprojects.com/en/master/patterns/errorpages/ | |
8 | + | |
9 | +@bp.app_errorhandler(403) | |
10 | +def forbidden_error(error): | |
11 | + error_title = "Page Interdite" | |
12 | + return render_template('error.html', error_title=error_title, error_msg=error), 403 | |
13 | + | |
14 | + | |
15 | +@bp.app_errorhandler(404) | |
16 | +def not_found_error(error): | |
17 | + error_title = "Page Introuvable." | |
18 | + return render_template('error.html', error_title=error_title, error_msg=error), 404 | |
19 | + | |
20 | + | |
21 | +@bp.app_errorhandler(405) | |
22 | +def method_error(error): | |
23 | + error_title = "Erreur de Méthode." | |
24 | + return render_template('error.html', error_title=error_title, error_msg=error), 405 | |
25 | + | |
26 | + | |
27 | +@bp.app_errorhandler(500) | |
28 | +def internal_error(error): | |
29 | + db.session.rollback() | |
30 | + error_title = "Erreur Interne. Administrateur Prévenu." | |
31 | + return render_template('error.html', error_title=error_title, error_msg=error), 500 | ... | ... |
app/main/routes.py
1 | 1 | import json |
2 | 2 | |
3 | -from flask import render_template, make_response | |
3 | +from flask import render_template, make_response, current_app, redirect, url_for, request | |
4 | +from flask_login import login_required, current_user | |
4 | 5 | |
5 | 6 | from . import bp |
6 | 7 | |
7 | 8 | from app.models import Agent, Project, Service, Capacity, Period |
8 | -from .. import db_mgr | |
9 | +from app import db_mgr | |
10 | +from app.auth.routes import role_required | |
11 | + | |
12 | + | |
13 | +@bp.before_request | |
14 | +def site_login(): | |
15 | + try: | |
16 | + if current_app.config['SITE_LOGIN'] \ | |
17 | + and not current_user.is_authenticated: | |
18 | + return redirect(url_for('auth.login')) | |
19 | + except KeyError: | |
20 | + # no such config, juste ignore | |
21 | + pass | |
22 | + | |
23 | +@bp.before_request | |
24 | +def catch_all_route(): | |
25 | + current_app.logger.info(f"{request.method} {request.path}") | |
9 | 26 | |
10 | 27 | |
11 | 28 | @bp.route('/') |
... | ... | @@ -14,6 +31,7 @@ def index(): |
14 | 31 | |
15 | 32 | |
16 | 33 | @bp.route('/services') |
34 | +@role_required('service') | |
17 | 35 | def services(): |
18 | 36 | # get services list |
19 | 37 | all_services = Service.query.order_by(Service.name).all() |
... | ... | @@ -24,6 +42,7 @@ def services(): |
24 | 42 | |
25 | 43 | |
26 | 44 | @bp.route('/projects') |
45 | +@role_required('project') | |
27 | 46 | def projects(): |
28 | 47 | # get projects list |
29 | 48 | all_projects = Project.query.order_by(Project.name).all() |
... | ... | @@ -34,6 +53,7 @@ def projects(): |
34 | 53 | |
35 | 54 | |
36 | 55 | @bp.route('/agents') |
56 | +@role_required('project') | |
37 | 57 | def agents(): |
38 | 58 | # get agents list |
39 | 59 | all_agents = Agent.query.order_by(Agent.firstname).all() |
... | ... | @@ -44,6 +64,7 @@ def agents(): |
44 | 64 | |
45 | 65 | |
46 | 66 | @bp.route('/capacities') |
67 | +@login_required | |
47 | 68 | def capacities(): |
48 | 69 | # get capacities list |
49 | 70 | all_capacities = Capacity.query.order_by(Capacity.name).all() |
... | ... | @@ -54,6 +75,7 @@ def capacities(): |
54 | 75 | |
55 | 76 | |
56 | 77 | @bp.route('/periods') |
78 | +@login_required | |
57 | 79 | def periods(): |
58 | 80 | # get capacities list |
59 | 81 | all_periods = Period.query.order_by(Period.name).all() |
... | ... | @@ -64,11 +86,13 @@ def periods(): |
64 | 86 | |
65 | 87 | |
66 | 88 | @bp.route('/charge/add') |
89 | +@role_required('service') | |
67 | 90 | def charge_add(): |
68 | 91 | return render_template('charge.html', subtitle="Affecter un agent") |
69 | 92 | |
70 | 93 | |
71 | 94 | @bp.route('/charge/agent/<agent_id>') |
95 | +@role_required('service') | |
72 | 96 | def charge_agent(agent_id): |
73 | 97 | agent_charges = [] |
74 | 98 | for [period, charge] in db_mgr.charges_by_agent(agent_id): |
... | ... | @@ -80,8 +104,10 @@ def charge_agent(agent_id): |
80 | 104 | |
81 | 105 | |
82 | 106 | @bp.route('/agent/<agent_id>') |
107 | +@role_required('agent') | |
83 | 108 | def agent(agent_id): |
84 | - agent = Agent.query.get(int(agent_id)) | |
109 | + # TODO: am i the agent, the service or project manager , or the admin ? | |
110 | + this_agent = Agent.query.get(int(agent_id)) | |
85 | 111 | return render_template('agent.html', |
86 | - agent=agent, | |
112 | + agent=this_agent, | |
87 | 113 | subtitle="{} {}".format(agent.firstname, agent.secondname)) | ... | ... |
app/models.py
1 | -from flask_login import UserMixin | |
2 | 1 | from flask_sqlalchemy import SQLAlchemy |
3 | 2 | |
4 | 3 | db = SQLAlchemy() |
5 | 4 | |
6 | 5 | |
7 | -class User(UserMixin, db.Model): | |
8 | - id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy | |
9 | - email = db.Column(db.String(100), unique=True) | |
10 | - name = db.Column(db.String(100)) | |
11 | - login = db.Column(db.String(100), unique=True) | |
12 | - password = db.Column(db.String(100)) | |
13 | - | |
14 | - def __repr__(self): | |
15 | - return "i: {}, n: {}, e: {}, l: {}".format(self.id, self.name, self.email, self.login) | |
16 | - | |
17 | - | |
18 | 6 | class Agent(db.Model): |
19 | 7 | id = db.Column(db.Integer, primary_key=True) |
20 | 8 | firstname = db.Column(db.String(100)) | ... | ... |
app/static/css/style.css
... | ... | @@ -57,3 +57,22 @@ nav.sidebar a.disabled { |
57 | 57 | padding-bottom: 15px; |
58 | 58 | border-bottom: 1px solid #ddd |
59 | 59 | } |
60 | + | |
61 | +/* -- - - - - - - - - - per site icon settings - - - - - - - - - -- */ | |
62 | + | |
63 | +.admin-icon { | |
64 | + filter: hue-rotate(300deg) saturate(1000%); | |
65 | +} | |
66 | + | |
67 | +.public-icon { | |
68 | + filter: saturate(200%); | |
69 | + /* hue-rotate(300deg) */ | |
70 | +} | |
71 | + | |
72 | +.dev-icon { | |
73 | + filter: hue-rotate(200deg) saturate(1000%); | |
74 | +} | |
75 | + | |
76 | +.dev2-icon { | |
77 | + filter: hue-rotate(100deg) saturate(1000%); | |
78 | +} | ... | ... |
app/templates/base_page.html
... | ... | @@ -9,8 +9,8 @@ |
9 | 9 | <body> |
10 | 10 | <nav class="navbar navbar-dark sticky-top bg-dark navbar-expand-lg p-1"> |
11 | 11 | <a class="navbar-brand col-sm-3 col-md-2 " href="{{url_for('main.index')}}"> |
12 | - <img class="m-0 mr-1" height="30" src="{{ url_for('static', filename='img/pdc-ico.png') }}" width="30"/> | |
13 | - Plan de Charges</a> | |
12 | + <img class="m-0 mr-1 {{config.PDC_SITE_CLASS}}" src="{{ url_for('static', filename='img/pdc-ico.png') }}" height="30" width="30"/> | |
13 | + {{config.PDC_APP_NAME}} - {{config.PDC_SITE_NAME}}</a> | |
14 | 14 | <ul class="navbar-nav ml-auto"> |
15 | 15 | {% if not current_user.is_anonymous %} |
16 | 16 | <li class="nav-item"> | ... | ... |
app/templates/copy.html
app/templates/flash-messages.html
... | ... | @@ -3,7 +3,7 @@ |
3 | 3 | <div id="messages" class="mt-1"> |
4 | 4 | {% for level, message in messages %} |
5 | 5 | {% if level == 'message' %} |
6 | - <div class="alert alert-primary}" role="alert">{{ message }} | |
6 | + <div class="alert alert-primary" role="alert">{{ message }} | |
7 | 7 | {% else %} |
8 | 8 | <div class="alert alert-{{level}}" role="alert">{{ message }} |
9 | 9 | {% endif %} | ... | ... |
app/templates/heads.html
... | ... | @@ -2,4 +2,4 @@ |
2 | 2 | <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"> |
3 | 3 | <link href="{{ url_for('static', filename='img/pdc-ico.png') }}" rel="icon"/> |
4 | 4 | <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" type="text/css"/> |
5 | - <title>{% if title %} {{title}} {% else %}Plan de Charge{%endif%}</title> | |
6 | 5 | \ No newline at end of file |
6 | + <title>{% if title %} {{title}} {% else %}{{config.PDC_APP_NAME}} - {{config.PDC_SITE_NAME}}{%endif%}</title> | |
7 | 7 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,28 @@ |
1 | +"""empty message | |
2 | + | |
3 | +Revision ID: 4d5e5e207063 | |
4 | +Revises: 6f7e51f380eb | |
5 | +Create Date: 2021-03-26 18:34:03.768661 | |
6 | + | |
7 | +""" | |
8 | +from alembic import op | |
9 | +import sqlalchemy as sa | |
10 | + | |
11 | + | |
12 | +# revision identifiers, used by Alembic. | |
13 | +revision = '4d5e5e207063' | |
14 | +down_revision = '6f7e51f380eb' | |
15 | +branch_labels = None | |
16 | +depends_on = None | |
17 | + | |
18 | + | |
19 | +def upgrade(): | |
20 | + # ### commands auto generated by Alembic - please adjust! ### | |
21 | + op.add_column('user', sa.Column('role', sa.Integer(), nullable=True)) | |
22 | + # ### end Alembic commands ### | |
23 | + | |
24 | + | |
25 | +def downgrade(): | |
26 | + # ### commands auto generated by Alembic - please adjust! ### | |
27 | + op.drop_column('user', 'role') | |
28 | + # ### end Alembic commands ### | ... | ... |
... | ... | @@ -0,0 +1,21 @@ |
1 | +#/bin/env sh | |
2 | + | |
3 | +# | |
4 | +# Shell wrapper to locally run the flask app as any user: | |
5 | +# | |
6 | +# sudo -u www-data ./pdc_web.sh | |
7 | +# | |
8 | +# Of course, any prerequisites, installation procedure and configuration files may have been fullfilled. | |
9 | +# See README.md and INSTALL.md for more details | |
10 | +# | |
11 | + | |
12 | +SCRIPT_PATH=$(readlink -f $0) | |
13 | +SCRIPT_DIR=$(dirname $SCRIPT_PATH) | |
14 | + | |
15 | +cd $SCRIPT_DIR | |
16 | + | |
17 | +[ ! -f ./venv/bin/activate ] && echo "Create python virtual env ./venv/" && exit | |
18 | +[ ! -f .flaskenv ] && echo "Create .flaskenv file" && exit | |
19 | + | |
20 | +. ./venv/bin/activate | |
21 | +flask run | ... | ... |
resources/db_config.py
resources/farm-index.html
... | ... | @@ -75,10 +75,10 @@ |
75 | 75 | <h1 class="h3 font-weight-normal my-5">Plan de Charge</h1> |
76 | 76 | |
77 | 77 | <p class="lead"> |
78 | - Place centrale de redirection vers les différents sites de la ferme d'applications sur le site de l'Irap. | |
78 | + Les différentes instances de l'application hébergées à l'Irap: | |
79 | 79 | </p> |
80 | 80 | <p class="lead"> |
81 | - <a class="btn btn-lg btn-secondary" href="https://plan-de-charges.irap.omp.eu/public/">Pour tous</a> | |
81 | + <a class="btn btn-lg btn-secondary" href="https://plan-de-charges.irap.omp.eu/public/">Public</a> | |
82 | 82 | <a class="btn btn-lg btn-secondary" href="https://plan-de-charges.irap.omp.eu/irap/">Irap</a> |
83 | 83 | </p> |
84 | 84 | </main> | ... | ... |
pdc_config.py renamed to resources/pdc_config.py
... | ... | @@ -10,6 +10,38 @@ root_dir = os.path.abspath(os.path.dirname(__file__)) |
10 | 10 | |
11 | 11 | class Config(object): |
12 | 12 | SECRET_KEY = 'dev' |
13 | + | |
14 | + # Please change the following to fit you own site parameters | |
15 | + # | |
16 | + PDC_APP_NAME = 'Plan de Charge' | |
17 | + PDC_SITE_NAME = 'NO_SITE' # choose among IRAP, PUBLIC, ... | |
18 | + PDC_SITE_CLASS = 'public-icon' # choose among admin-icon, public-icon | |
19 | + PDC_LOGS_LEVEL = 'DEBUG' # choose within DEBUG, INFO, WARN, ERROR ( more levels in logging module ) | |
20 | + PDC_LOGS_DIR = os.path.join(root_dir, 'logs') | |
21 | + PDC_LOGS_FILE = os.path.join(PDC_LOGS_DIR, 'pdc.log') | |
22 | + | |
23 | + # Uncomment for role access control | |
24 | + # if True, will disable any role control on routes | |
25 | + # note that this doesnt disable the @login_required | |
26 | + # | |
27 | + # ROLE_DISABLED = False | |
28 | + | |
29 | + # Uncomment for site access control | |
30 | + # if True, will force login access on any site page | |
31 | + # note that this doesnt disable the @login_required | |
32 | + # | |
33 | + # SITE_LOGIN = False | |
34 | + | |
35 | + # | |
36 | + # No need to Edit below | |
37 | + # | |
38 | + | |
39 | + # You can force logging to stdout in production environment | |
40 | + # (make sure your httpd/wsgi server can redirect to log files) | |
41 | + LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT') | |
42 | + if LOG_TO_STDOUT and LOG_TO_STDOUT.upper() == "true".upper(): | |
43 | + LOG_TO_STDOUT = True | |
44 | + | |
13 | 45 | SQLALCHEMY_TRACK_MODIFICATIONS = False |
14 | 46 | |
15 | 47 | # Trying to set specific db uri from ./db_config.py |
... | ... | @@ -19,6 +51,12 @@ class Config(object): |
19 | 51 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ |
20 | 52 | 'sqlite:///' + os.path.join(root_dir, 'pdc_app.db') |
21 | 53 | |
54 | + try: | |
55 | + LESIA_AGENTS_DB_URI = mysql_lesia_uri | |
56 | + except NameError: | |
57 | + LESIA_AGENTS_DB_URI = os.environ.get('LESIA_AGENTS_DB_URI') or \ | |
58 | + 'sqlite:///' + os.path.join(root_dir, 'lesia.db') | |
59 | + | |
22 | 60 | with open(os.path.join(root_dir, 'VERSION.txt')) as version_file: |
23 | 61 | VERSION = version_file.read().strip() |
24 | 62 | |
... | ... | @@ -36,11 +74,16 @@ class DevConfig(Config): |
36 | 74 | except NameError: |
37 | 75 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ |
38 | 76 | 'sqlite:///' + os.path.join(root_dir, 'pdc_app_dev.db') |
77 | + # ignores @login_required decorator | |
78 | + LOGIN_DISABLED = True | |
79 | + # ignores @role_required decorator | |
80 | + ROLE_DISABLED = True | |
39 | 81 | |
40 | 82 | |
41 | 83 | class TestConfig(Config): |
42 | 84 | TESTING = True |
43 | 85 | DEBUG = True |
86 | + PDC_LOGS_LEVEL = 'ERROR' # choose within DEBUG, INFO, WARN, ERROR ( more levels in logging module ) | |
44 | 87 | # Trying to set specific db uri from ./db_config.py |
45 | 88 | try: |
46 | 89 | SQLALCHEMY_DATABASE_URI = sqlalchemy_testdb_uri |
... | ... | @@ -48,6 +91,8 @@ class TestConfig(Config): |
48 | 91 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ |
49 | 92 | 'sqlite:///' + os.path.join(root_dir, 'pdc_app_test.db') |
50 | 93 | # ignores @login_required decorator |
51 | - # LOGIN_DISABLED = True | |
94 | + LOGIN_DISABLED = True | |
95 | + # ignores @role_required decorator | |
96 | + ROLE_DISABLED = True | |
52 | 97 | |
53 | 98 | # vim: tw=0 | ... | ... |
tests/backend_tests.py
1 | 1 | import unittest |
2 | 2 | from pdc_config import TestConfig |
3 | -from app import create_app, db_mgr | |
3 | +from app import create_app, db_mgr, db | |
4 | +from app.auth.models import User | |
4 | 5 | |
5 | 6 | |
6 | 7 | class BaseTestCase(unittest.TestCase): |
... | ... | @@ -9,6 +10,10 @@ class BaseTestCase(unittest.TestCase): |
9 | 10 | self.app = create_app(TestConfig) |
10 | 11 | self.app_context = self.app.app_context() |
11 | 12 | self.app_context.push() |
13 | + db.create_all() | |
14 | + admin = User(email='admin@nowhere.org', name='admin', login='admin', password='admin', role='admin') | |
15 | + db.session.add(admin) | |
16 | + db.session.commit() | |
12 | 17 | |
13 | 18 | def tearDown(self): |
14 | 19 | self.app_context.pop() |
... | ... | @@ -27,3 +32,20 @@ class DbMgrTestCase(BaseTestCase): |
27 | 32 | def test_charges_by_agent(self): |
28 | 33 | all_charges = db_mgr.charges_by_agent(355) |
29 | 34 | self.assertEqual(6, len(all_charges)) |
35 | + | |
36 | + | |
37 | +class AuthModelTestCase(BaseTestCase): | |
38 | + | |
39 | + def test_setrole(self): | |
40 | + admin = User.query.filter(User.name == 'admin').one_or_none() | |
41 | + admin.set_role("ADMIN") | |
42 | + db.session.commit() | |
43 | + admin = User.query.filter(User.name == 'admin').one_or_none() | |
44 | + self.assertTrue(admin is not None) | |
45 | + self.assertTrue(admin.has_role("ADMIN")) | |
46 | + self.assertFalse(admin.has_role("SERVICE")) | |
47 | + | |
48 | + def test_setrole_valueerror(self): | |
49 | + admin = User(email='me@nowhere.org', name='me', login='me', password='me', role='admin') | |
50 | + with self.assertRaises(ValueError) as ve: | |
51 | + admin.set_role("NOSUCHROLE") | ... | ... |