Commit 9332a554747a59bbb348f0f82bad0a75ca89bd7f

Authored by hitier
2 parents ef44d9ee 38637852

File logging and route access control

.gitignore
... ... @@ -11,3 +11,4 @@ _static
11 11 _templates
12 12 *db
13 13 db_config.py
  14 +logs
... ...
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
1   -0.2.pre-5
  1 +0.2.pre-6
... ...
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
... ...
app/auth/models.py 0 → 100644
... ... @@ -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  
... ...
app/errors/__init__.py 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +from flask import Blueprint
  2 +
  3 +bp = Blueprint('errors', __name__, url_prefix='/auth', template_folder='templates')
  4 +
  5 +from . import handlers
... ...
app/errors/handlers.py 0 → 100644
... ... @@ -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/errors/templates/error.html 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +{% extends "base_page.html" %}
  2 +
  3 +{% block content %}
  4 +<h2 class="error_title">{{ error_title }}</h2>
  5 +<div class="error_msg">{{ error_msg }}</div>
  6 +<a href="{{url_for('main.index')}}">Retour à l'accueil</a>
  7 +{% endblock %}
... ...
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
1 1 <p class="text-muted text-center">
2   - Plan de Charge - {{ config.VERSION }}<br/>
  2 + {{ config.PDC_APP_NAME }} - {{ config.VERSION }}<br/>
3 3 © 2021 IRAP
4 4 </p>
... ...
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
... ...
migrations/versions/4d5e5e207063_.py 0 → 100644
... ... @@ -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 ###
... ...
pdc_web.sh 0 → 100755
... ... @@ -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
... ... @@ -39,6 +39,7 @@ POSTGRESTEST = {
39 39 }
40 40 postgres_test_uri = 'postgresql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % POSTGRESTEST
41 41  
  42 +# The lesia mysql agents database
42 43 MYSQL_LESIA = {
43 44 'user': 'mysql',
44 45 'pw': 'mysql',
... ...
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")
... ...