Commit e6f5e7fe authored by Pietro Albini's avatar Pietro Albini

Merge branch 'actions' into 'develop'

Aggiungi un registro delle azioni recenti

See merge request !17
parents fed492cc ad488876
Pipeline #103 passed with stage
in 0 seconds
......@@ -18,13 +18,14 @@ import os
import flask
from . import actions
from . import auth
from . import cache
from . import db
from . import pages
from . import utils
from . import download
from . import navbar
from . import pages
from . import utils
from . import utils
......@@ -55,12 +56,10 @@ def create_app(data_path):
utils.prepare_app(app)
app.download = download.Downloads(data_path)
app.register_blueprint(
app.download.prepare_blueprint(app),
url_prefix="/download",
)
app.register_blueprint(auth.prepare_blueprint(app), url_prefix="/+auth")
app.register_blueprint(app.download.prepare_blueprint(app), url_prefix="/download")
app.register_blueprint(actions.prepare_blueprint(app), url_prefix="/+actions")
app.register_blueprint(auth.prepare_blueprint(app), url_prefix="/+auth")
app.register_blueprint(pages.prepare_blueprint(app))
nav = navbar.Navbar()
......
# Source code of the Ubuntu-it website
# Copyright (C) 2018 Pietro Albini <pietroalbini@ubuntu.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; witout even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
import time
import flask
from uitwww import auth
ACTIONS_PER_PAGE = 25
class PageNotFoundError(BaseException):
pass
def log(message, session=None):
"""Log an action into the database"""
date = int(time.time())
if session is None:
session = flask.g.auth_id
flask.current_app.db.update(
"INSERT INTO actions (session, date, message) VALUES (?, ?, ?);",
session, date, message,
)
def list_actions(page=None, session=None):
"""List all the available actions"""
result = {
"actions": []
}
args = []
where = ""
if session is not None:
where = "WHERE a.session = ?"
args.append(session)
limit = ""
if page is not None:
page = page - 1
total = flask.current_app.db.query(
"SELECT COUNT(*) FROM actions %s;" % where, *args
)[0][0]
if page < 0 or page >= math.ceil(total / ACTIONS_PER_PAGE):
raise PageNotFoundError
result["current_page"] = page + 1
result["has_prev"] = page != 0
result["has_next"] = page + 1 < math.ceil(total / ACTIONS_PER_PAGE)
limit = "LIMIT ? OFFSET ?"
args += [ACTIONS_PER_PAGE, page * ACTIONS_PER_PAGE]
rows = flask.current_app.db.query(
"SELECT s.nickname, s.ip, s.id, a.date, a.message FROM actions AS a "
"INNER JOIN auth_sessions AS s ON (s.id = a.session) %s "
"ORDER BY date DESC %s;" % (where, limit), *args
)
for row in rows:
result["actions"].append({
"nickname": row[0],
"ip": row[1],
"session": row[2],
"date": row[3],
"message": row[4],
})
return result
def prepare_blueprint(app):
"""Prepare the auth blueprint"""
bp = flask.Blueprint("actions", __name__)
@bp.route("/")
@bp.route("/<int:page>")
@auth.permission("actions.show")
def show(page=1):
try:
actions = list_actions(page=page)
except PageNotFoundError:
return flask.abort(404)
return flask.render_template(
"actions/show.html",
actions=actions,
)
return bp
......@@ -43,18 +43,19 @@ class Sessions:
id = str(uuid.uuid4())
ip = flask.request.remote_addr
now = int(time.time())
self.db.update(
"INSERT INTO auth_sessions (id, nickname, teams, ip, expires_at) "
"VALUES (?, ?, ?, ?, ?)", id, nickname, ",".join(teams), ip,
int(time.time()) + SESSION_EXPIRES_AFTER,
"INSERT INTO auth_sessions (id, nickname, teams, ip, created_at, "
"expires_at) VALUES (?, ?, ?, ?, ?, ?)", id, nickname,
",".join(teams), ip, now, now + SESSION_EXPIRES_AFTER,
)
return id
def check(self, id):
"""Check if a session is valid and return its data"""
data = self.db.query(
"SELECT nickname, teams, ip, expires_at FROM auth_sessions "
"WHERE id = ?", id,
"SELECT nickname, teams, ip, expires_at, created_at "
"FROM auth_sessions WHERE id = ?", id,
)
if not data:
raise SessionError("La tua sessione è scaduta, accedi di nuovo.")
......@@ -65,7 +66,6 @@ class Sessions:
expires_at = data[0][3]
if expires_at is None or expires_at < time.time():
self.delete(id)
raise SessionError("La tua sessione è scaduta, accedi di nuovo.")
else:
# Bump the session every time a new page is visited
......@@ -82,50 +82,58 @@ class Sessions:
"teams": data[0][1].split(","),
}
def delete(self, id):
"""Delete a session"""
self.db.update("DELETE FROM auth_sessions WHERE id = ?;", id)
def disable(self, id):
"""Disable a session"""
self.db.update(
"UPDATE auth_sessions SET expires_at = ? WHERE id = ?;",
int(time.time()), id,
)
def delete_all(self, except_id):
"""Delete every session except this one"""
self.db.update("DELETE FROM auth_sessions WHERE id != ?;", except_id)
def disable_all(self, except_id):
"""Disable every active session except this one"""
now = int(time.time())
self.db.update(
"UPDATE auth_sessions SET expires_at = ? WHERE id != ? AND "
"expires_at IS NOT NULL AND expires_at >= ?;",
now, except_id, now,
)
def all(self):
"""Return all the sessions"""
rows = self.db.query("SELECT id, nickname, teams, ip, expires_at FROM auth_sessions;")
now = time.time()
rows = self.db.query(
"SELECT id, nickname, teams, ip, expires_at, created_at "
"FROM auth_sessions WHERE expires_at IS NOT NULL "
"AND expires_at >= ?;", now,
)
result = []
for row in rows:
# Delete expired sessions
if row[0] is None or row[4] < now:
self.delete(row[0])
continue
result.append({
"id": row[0],
"nickname": row[1],
"teams": row[2].split(","),
"ip": row[3],
"expires_at": row[4],
"created_at": row[5],
})
return result
def get(self, id):
"""Return details about a session"""
row = self.db.query("SELECT nickname, teams, ip, expires_at FROM auth_sessions WHERE id = ?;", id)
row = self.db.query(
"SELECT nickname, teams, ip, expires_at, created_at "
"FROM auth_sessions WHERE id = ?;", id,
)
if row:
# Delete expired sessions
if row[0][3] is None or row[0][3] < time.time():
self.delete(id)
return
return {
"id": id,
"nickname": row[0][0],
"teams": row[0][1].split(","),
"ip": row[0][2],
"expires_at": row[0][3],
"created_at": row[0][4],
"active": row[0][3] >= time.time(),
}
def count(self):
......@@ -197,6 +205,8 @@ def permission(perms):
def prepare_blueprint(app):
"""Prepare the auth blueprint"""
from uitwww import actions
bp = flask.Blueprint("auth", __name__)
oid = flask_openid.OpenID(
......@@ -238,6 +248,9 @@ def prepare_blueprint(app):
flask.session["auth"] = sessions.create(resp.nickname, teams)
flask.flash("Benvenuto %s!" % resp.nickname, "success")
actions.log("Accesso effettuato", session=flask.session["auth"])
return flask.redirect(flask.url_for("pages.index"))
@bp.route("/login")
......@@ -258,7 +271,7 @@ def prepare_blueprint(app):
@bp.route("/logout")
@permission("auth.logout")
def logout():
sessions.delete(flask.session["auth"])
sessions.disable(flask.session["auth"])
del flask.session["auth"]
flask.flash("La sessione è stata terminata correttamente.", "success")
......@@ -276,7 +289,8 @@ def prepare_blueprint(app):
@bp.route("/sessions/+all/revoke")
@permission("auth.sessions.manage")
def sessions_revoke_all():
sessions.delete_all(flask.g.auth_id)
actions.log("Disabilitate tutte le altre sessioni")
sessions.disable_all(flask.g.auth_id)
return flask.redirect(flask.url_for(".sessions_list"))
@bp.route("/sessions/<id>")
......@@ -296,10 +310,15 @@ def prepare_blueprint(app):
continue
others.append(session)
user_actions = []
if app.permissions.check("actions.show"):
user_actions = actions.list_actions(session=id)
return flask.render_template(
"auth/session.html",
session=data,
others=others,
actions=user_actions,
)
@bp.route("/sessions/<id>/revoke")
......@@ -308,7 +327,11 @@ def prepare_blueprint(app):
"auth.sessions.own": lambda id: flask.g.auth_id == id,
})
def sessions_revoke(id):
sessions.delete(id)
actions.log(
'Disabilitata <a href="%s">una sessione</a>'
% flask.url_for("auth.sessions_show", id=id)
)
sessions.disable(id)
return flask.redirect(flask.url_for(".sessions_list"))
return bp
permissions:
- actions.show
- auth.logout
- auth.sessions.manage
- auth.sessions.own
......
......@@ -85,4 +85,16 @@ MIGRATIONS = [
("add_auth_sessions_expires_at_column", """
ALTER TABLE auth_sessions ADD COLUMN expires_at INTEGER;
"""),
("add_auth_sessions_created_at_column", """
ALTER TABLE auth_sessions ADD COLUMN created_at INTEGER;
"""),
("add_actions_table", """
CREATE TABLE actions (
session TEXT NOT NULL,
date INTEGER NOT NULL,
message TEXT NOT NULL
);
"""),
]
{# Source code of the Ubuntu-it website
# Copyright (C) 2018 Pietro Albini <pietroalbini@ubuntu.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; witout even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% macro render_actions(actions, own=False) %}
<div class="table">
<table>
<tr>
<th></th>
{% if not own %}
<th>Nome utente</th>
{% endif %}
<th>Data e ora</th>
{% if not own %}
<th>Indirizzo IP</th>
{% endif %}
</tr>
{% for action in actions.actions %}
<tr>
<td>{{ action.message|safe }}</td>
{% if not own %}
<td>{{ action.nickname }}</td>
{% endif %}
<td>{{ action["date"]|format_timestamp }}</td>
{% if not own %}
<td>
{% if (g.auth_name == action.nickname and permission("auth.sessions.own")) or permission("auth.sessions.manage") %}
<a href="{{ url_for("auth.sessions_show", id=action.session) }}">
{{ action.ip }}
</a>
{% else %}
{{ action.ip }}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
<p class="text-center">
{% if actions.current_page %}
{% if actions.has_prev %}
<a href="{{ url_for(request.endpoint, page=actions.current_page - 1) }}">
Pagina precedente
</a>
-
{% endif %}
<b>Pagina {{ actions.current_page }}</b>
{% if actions.has_next %}
-
<a href="{{ url_for(request.endpoint, page=actions.current_page + 1) }}">
Pagina successiva
</a>
{% endif %}
{% endif %}
</p>
{% endmacro %}
{# Source code of the Ubuntu-it website
# Copyright (C) 2018 Pietro Albini <pietroalbini@ubuntu.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; witout even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "layout.html" %}
{% from "actions/macros.html" import render_actions with context %}
{% block title %}Azioni recenti{% endblock %}
{% block content %}
<div class="page">
<div class="row">
<div class="col">
<h1>Azioni recenti</h1>
{{ render_actions(actions) }}
</div>
</div>
</div>
{% endblock %}
......@@ -16,6 +16,7 @@
#}
{% extends "layout.html" %}
{% from "actions/macros.html" import render_actions with context %}
{% block title %}Dettagli sessione{% endblock %}
......@@ -31,12 +32,24 @@
<td>Nome utente</td>
<td><b>{{ session.nickname }}</b></td>
</tr>
<tr>
<td>Stato</td>
<td>{% if session.active -%}
<span class="text-green">Attiva</span>
{%- else -%}
<span class="text-red">Scaduta</span>
{%- endif %}</td>
</tr>
<tr>
<td>Indirizzo IP</td>
<td>{{ session.ip }}</td>
</tr>
<tr>
<td>Scadenza</td>
<td>Inizio</td>
<td>{{ session.created_at|format_timestamp }}</td>
</tr>
<tr>
<td>{% if session.active %}Scadenza{% else %}Fine{% endif %}</td>
<td>{{ session.expires_at|format_timestamp }}</td>
</tr>
<tr>
......@@ -53,7 +66,7 @@
</table>
</div>
{% if g.auth_id != session.id %}
{% if g.auth_id != session.id and session.active %}
<a class="btn" href="{{ url_for(".sessions_revoke", id=session.id) }}">
Disabilita sessione
</a>
......@@ -63,11 +76,16 @@
{% if others %}
<div class="row">
<div class="col">
{% if session.active %}
<h2>Altre sessioni dell'utente</h2>
{% else %}
<h2>Sessioni correnti dell'utente</h2>
{% endif %}
<div class="table">
<table>
<tr>
<th>Indirizzo IP</th>
<th>Inizio</th>
<th>Scadenza</th>
<th></th>
<th></th>
......@@ -75,6 +93,7 @@
{% for session in others %}
<tr>
<td>{{ session.ip }}</td>
<td>{{ session.created_at|format_timestamp }}
<td>{{ session.expires_at|format_timestamp }}
<td>
<a href="{{ url_for(".sessions_show", id=session.id) }}">
......@@ -98,4 +117,15 @@
</div>
{% endif %}
</div>
{% if permission("actions.show") and actions %}
<div class="page">
<div class="row">
<div class="col">
<h2>Azioni eseguite in questa sessione</h2>
{{ render_actions(actions, own=True) }}
</div>
</div>
</div>
{% endif %}
{% endblock %}
......@@ -40,6 +40,7 @@
<tr>
<th>Nome utente</th>
<th>Indirizzo IP</th>
<th>Inizio</th>
<th>Scadenza</th>
<th></th>
<th></th>
......@@ -48,6 +49,7 @@
<tr>
<td>{{ session.nickname }}</td>
<td>{{ session.ip }}</td>
<td>{{ session.created_at|format_timestamp }}</td>
<td>{{ session.expires_at|format_timestamp }}</td>
<td>
<a href="{{ url_for(".sessions_show", id=session.id) }}">
......
......@@ -54,6 +54,11 @@
Sessioni attive: {{ g.auth_sessions_count }}
</a></li>
{% endif %}
{% if permission("actions.show") %}
<li><a href="{{ url_for("actions.show") }}">
Azioni recenti
</a></li>
{% endif %}
</ul>
<ul class="right">
{% if permission("auth.sessions.own") %}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment