Commit 91fc90c4 authored by Alessandro Viprati's avatar Alessandro Viprati

Merge branch 'develop' into 'scopri-ubuntu'

# Conflicts:
#   uitwww/data/downloads.toml
parents c42184be e6f5e7fe
Pipeline #109 running with stage
...@@ -18,13 +18,14 @@ import os ...@@ -18,13 +18,14 @@ import os
import flask import flask
from . import actions
from . import auth from . import auth
from . import cache from . import cache
from . import db from . import db
from . import pages
from . import utils
from . import download from . import download
from . import navbar from . import navbar
from . import pages
from . import utils
from . import utils from . import utils
...@@ -55,12 +56,10 @@ def create_app(data_path): ...@@ -55,12 +56,10 @@ def create_app(data_path):
utils.prepare_app(app) utils.prepare_app(app)
app.download = download.Downloads(data_path) 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)) app.register_blueprint(pages.prepare_blueprint(app))
nav = navbar.Navbar() 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: ...@@ -43,18 +43,19 @@ class Sessions:
id = str(uuid.uuid4()) id = str(uuid.uuid4())
ip = flask.request.remote_addr ip = flask.request.remote_addr
now = int(time.time())
self.db.update( self.db.update(
"INSERT INTO auth_sessions (id, nickname, teams, ip, expires_at) " "INSERT INTO auth_sessions (id, nickname, teams, ip, created_at, "
"VALUES (?, ?, ?, ?, ?)", id, nickname, ",".join(teams), ip, "expires_at) VALUES (?, ?, ?, ?, ?, ?)", id, nickname,
int(time.time()) + SESSION_EXPIRES_AFTER, ",".join(teams), ip, now, now + SESSION_EXPIRES_AFTER,
) )
return id return id
def check(self, id): def check(self, id):
"""Check if a session is valid and return its data""" """Check if a session is valid and return its data"""
data = self.db.query( data = self.db.query(
"SELECT nickname, teams, ip, expires_at FROM auth_sessions " "SELECT nickname, teams, ip, expires_at, created_at "
"WHERE id = ?", id, "FROM auth_sessions WHERE id = ?", id,
) )
if not data: if not data:
raise SessionError("La tua sessione è scaduta, accedi di nuovo.") raise SessionError("La tua sessione è scaduta, accedi di nuovo.")
...@@ -65,7 +66,6 @@ class Sessions: ...@@ -65,7 +66,6 @@ class Sessions:
expires_at = data[0][3] expires_at = data[0][3]
if expires_at is None or expires_at < time.time(): if expires_at is None or expires_at < time.time():
self.delete(id)
raise SessionError("La tua sessione è scaduta, accedi di nuovo.") raise SessionError("La tua sessione è scaduta, accedi di nuovo.")
else: else:
# Bump the session every time a new page is visited # Bump the session every time a new page is visited
...@@ -82,50 +82,58 @@ class Sessions: ...@@ -82,50 +82,58 @@ class Sessions:
"teams": data[0][1].split(","), "teams": data[0][1].split(","),
} }
def delete(self, id): def disable(self, id):
"""Delete a session""" """Disable a session"""
self.db.update("DELETE FROM auth_sessions WHERE id = ?;", id) self.db.update(
"UPDATE auth_sessions SET expires_at = ? WHERE id = ?;",
int(time.time()), id,
)
def delete_all(self, except_id): def disable_all(self, except_id):
"""Delete every session except this one""" """Disable every active session except this one"""
self.db.update("DELETE FROM auth_sessions WHERE id != ?;", except_id) 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): def all(self):
"""Return all the sessions""" """Return all the sessions"""
rows = self.db.query("SELECT id, nickname, teams, ip, expires_at FROM auth_sessions;")
now = time.time() 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 = [] result = []
for row in rows: for row in rows:
# Delete expired sessions
if row[0] is None or row[4] < now:
self.delete(row[0])
continue
result.append({ result.append({
"id": row[0], "id": row[0],
"nickname": row[1], "nickname": row[1],
"teams": row[2].split(","), "teams": row[2].split(","),
"ip": row[3], "ip": row[3],
"expires_at": row[4], "expires_at": row[4],
"created_at": row[5],
}) })
return result return result
def get(self, id): def get(self, id):
"""Return details about a session""" """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: if row:
# Delete expired sessions
if row[0][3] is None or row[0][3] < time.time():
self.delete(id)
return
return { return {
"id": id, "id": id,
"nickname": row[0][0], "nickname": row[0][0],
"teams": row[0][1].split(","), "teams": row[0][1].split(","),
"ip": row[0][2], "ip": row[0][2],
"expires_at": row[0][3], "expires_at": row[0][3],
"created_at": row[0][4],
"active": row[0][3] >= time.time(),
} }
def count(self): def count(self):
...@@ -197,6 +205,8 @@ def permission(perms): ...@@ -197,6 +205,8 @@ def permission(perms):
def prepare_blueprint(app): def prepare_blueprint(app):
"""Prepare the auth blueprint""" """Prepare the auth blueprint"""
from uitwww import actions
bp = flask.Blueprint("auth", __name__) bp = flask.Blueprint("auth", __name__)
oid = flask_openid.OpenID( oid = flask_openid.OpenID(
...@@ -238,6 +248,9 @@ def prepare_blueprint(app): ...@@ -238,6 +248,9 @@ def prepare_blueprint(app):
flask.session["auth"] = sessions.create(resp.nickname, teams) flask.session["auth"] = sessions.create(resp.nickname, teams)
flask.flash("Benvenuto %s!" % resp.nickname, "success") flask.flash("Benvenuto %s!" % resp.nickname, "success")
actions.log("Accesso effettuato", session=flask.session["auth"])
return flask.redirect(flask.url_for("pages.index")) return flask.redirect(flask.url_for("pages.index"))
@bp.route("/login") @bp.route("/login")
...@@ -258,7 +271,7 @@ def prepare_blueprint(app): ...@@ -258,7 +271,7 @@ def prepare_blueprint(app):
@bp.route("/logout") @bp.route("/logout")
@permission("auth.logout") @permission("auth.logout")
def logout(): def logout():
sessions.delete(flask.session["auth"]) sessions.disable(flask.session["auth"])
del flask.session["auth"] del flask.session["auth"]
flask.flash("La sessione è stata terminata correttamente.", "success") flask.flash("La sessione è stata terminata correttamente.", "success")
...@@ -276,7 +289,8 @@ def prepare_blueprint(app): ...@@ -276,7 +289,8 @@ def prepare_blueprint(app):
@bp.route("/sessions/+all/revoke") @bp.route("/sessions/+all/revoke")
@permission("auth.sessions.manage") @permission("auth.sessions.manage")
def sessions_revoke_all(): 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")) return flask.redirect(flask.url_for(".sessions_list"))
@bp.route("/sessions/<id>") @bp.route("/sessions/<id>")
...@@ -296,10 +310,15 @@ def prepare_blueprint(app): ...@@ -296,10 +310,15 @@ def prepare_blueprint(app):
continue continue
others.append(session) others.append(session)
user_actions = []
if app.permissions.check("actions.show"):
user_actions = actions.list_actions(session=id)
return flask.render_template( return flask.render_template(
"auth/session.html", "auth/session.html",
session=data, session=data,
others=others, others=others,
actions=user_actions,
) )
@bp.route("/sessions/<id>/revoke") @bp.route("/sessions/<id>/revoke")
...@@ -308,7 +327,11 @@ def prepare_blueprint(app): ...@@ -308,7 +327,11 @@ def prepare_blueprint(app):
"auth.sessions.own": lambda id: flask.g.auth_id == id, "auth.sessions.own": lambda id: flask.g.auth_id == id,
}) })
def sessions_revoke(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 flask.redirect(flask.url_for(".sessions_list"))
return bp return bp
...@@ -114,9 +114,9 @@ archs = { amd64 = "cdimages-derivatives", i386 = "cdimages-derivatives" } ...@@ -114,9 +114,9 @@ archs = { amd64 = "cdimages-derivatives", i386 = "cdimages-derivatives" }
#description = "Ubuntu con tutti i programmi per lavorare con audio, video e immagini" #description = "Ubuntu con tutti i programmi per lavorare con audio, video e immagini"
#lts-support-years = 3 #lts-support-years = 3
#lts-only = true lts-only = true
#releases = ["latest", "lts"] releases = ["lts"]
#archs = { amd64 = "cdimages-studio", i386 = "cdimages-studio" } archs = { amd64 = "cdimages-studio", i386 = "cdimages-studio" }
......
permissions: permissions:
- actions.show
- auth.logout - auth.logout
- auth.sessions.manage - auth.sessions.manage
- auth.sessions.own - auth.sessions.own
......
...@@ -85,4 +85,16 @@ MIGRATIONS = [ ...@@ -85,4 +85,16 @@ MIGRATIONS = [
("add_auth_sessions_expires_at_column", """ ("add_auth_sessions_expires_at_column", """
ALTER TABLE auth_sessions ADD COLUMN expires_at INTEGER; 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 @@ ...@@ -16,6 +16,7 @@
#} #}
{% extends "layout.html" %} {% extends "layout.html" %}
{% from "actions/macros.html" import render_actions with context %}
{% block title %}Dettagli sessione{% endblock %} {% block title %}Dettagli sessione{% endblock %}
...@@ -31,12 +32,24 @@ ...@@ -31,12 +32,24 @@
<td>Nome utente</td> <td>Nome utente</td>
<td><b>{{ session.nickname }}</b></td> <td><b>{{ session.nickname }}</b></td>
</tr> </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> <tr>
<td>Indirizzo IP</td> <td>Indirizzo IP</td>
<td>{{ session.ip }}</td> <td>{{ session.ip }}</td>
</tr> </tr>
<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> <td>{{ session.expires_at|format_timestamp }}</td>
</tr> </tr>
<tr> <tr>
...@@ -53,7 +66,7 @@ ...@@ -53,7 +66,7 @@
</table> </table>
</div> </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) }}"> <a class="btn" href="{{ url_for(".sessions_revoke", id=session.id) }}">
Disabilita sessione Disabilita sessione
</a> </a>
...@@ -63,11 +76,16 @@ ...@@ -63,11 +76,16 @@
{% if others %} {% if others %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
{% if session.active %}
<h2>Altre sessioni dell'utente</h2> <h2>Altre sessioni dell'utente</h2>
{% else %}
<h2>Sessioni correnti dell'utente</h2>
{% endif %}
<div class="table"> <div class="table">
<table> <table>
<tr> <tr>
<th>Indirizzo IP</th> <th>Indirizzo IP</th>
<th>Inizio</th>
<th>Scadenza</th> <th>Scadenza</th>
<th></th> <th></th>
<th></th> <th></th>
...@@ -75,6 +93,7 @@ ...@@ -75,6 +93,7 @@
{% for session in others %} {% for session in others %}
<tr> <tr>
<td>{{ session.ip }}</td> <td>{{ session.ip }}</td>
<td>{{ session.created_at|format_timestamp }}
<td>{{ session.expires_at|format_timestamp }} <td>{{ session.expires_at|format_timestamp }}
<td> <td>
<a href="{{ url_for(".sessions_show", id=session.id) }}"> <a href="{{ url_for(".sessions_show", id=session.id) }}">
...@@ -98,4 +117,15 @@ ...@@ -98,4 +117,15 @@
</div> </div>
{% endif %} {% endif %}
</div> </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 %} {% endblock %}
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
<tr> <tr>
<th>Nome utente</th> <th>Nome utente</th>
<th>Indirizzo IP</th> <th>Indirizzo IP</th>
<th>Inizio</th>
<th>Scadenza</th> <th>Scadenza</th>
<th></th> <th></th>
<th></th> <th></th>
...@@ -48,6 +49,7 @@ ...@@ -48,6 +49,7 @@
<tr> <tr>
<td>{{ session.nickname }}</td> <td>{{ session.nickname }}</td>
<td>{{ session.ip }}</td> <td>{{ session.ip }}</td>
<td>{{ session.created_at|format_timestamp }}</td>
<td>{{ session.expires_at|format_timestamp }}</td> <td>{{ session.expires_at|format_timestamp }}</td>
<td> <td>
<a href="{{ url_for(".sessions_show", id=session.id) }}"> <a href="{{ url_for(".sessions_show", id=session.id) }}">
......
...@@ -54,6 +54,11 @@ ...@@ -54,6 +54,11 @@
Sessioni attive: {{ g.auth_sessions_count }} Sessioni attive: {{ g.auth_sessions_count }}
</a></li> </a></li>
{% endif %} {% endif %}
{% if permission("actions.show") %}
<li><a href="{{ url_for("actions.show") }}">
Azioni recenti
</a></li>
{% endif %}
</ul> </ul>
<ul class="right"> <ul class="right">
{% if permission("auth.sessions.own") %} {% 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