- Autorisointi.
- Käytettävyyden viilausta.
- Toiminnallisuuden täydentäminen (uusia ominaisuuksia).
- Kirjoita työhösi alustava asennusohje ja käyttöohje.
Tietokantasovellus-kurssin viidennen viikon materiaali sisältää linkkejä autorisointiin ja käytettävyyteen (mukaanlukien saavutettavuus) liittyen. Viidennen viikon aikana harjoitustyöhön tulee lisätä autorisointitoiminnallisuus (mikäli se puuttuu), parantaa sovelluksen käytettävyyttä (mukaanlukien saavutettavuus), sekä lisätä sovellukseen uutta toiminnallisuutta.
Huomaathan, että materiaali ei sisällä kaikkea harjoitustyöhön tarvittavaa.
Autorisointi
Tällä hetkellä harjoitustyössä pitäisi olla rekisteröitymis- ja kirjautumistoiminnallisuus, joista jälkimmäistä käytetään käyttäjän tunnistamiseen eli autentikointiin. Autentikointi on vain osa sovelluksen toimintaa -- käyttäjän tunnistamisen lisäksi pitää varmistaa, että käyttäjällä on oikeus tehdä niitä asioita, joita hän haluaa tehdä.
Autorisoinnilla tarkoitetaan varmistusta siitä, että käyttäjällä on oikeus hänen haluamaansa toimintoon.
Suoraviivainen autorisointi onnistuu flask-login
-kirjaston @login_required
-määreellä, jonka avulla suojataan resursseja. Mikäli resurssiin liitetään kyseinen määre, tulee resurssia pyytävän käyttäjän olla kirjautunut.
Alla olevassa kolmannelta viikolta tutussa lähdekoodissa kuka takansa voi listata tehtävät, mutta vain kirjautuneella käyttäjällä on pääsy osoitteeseen /tasks/new/
.
from flask import render_template, request, redirect, url_for
from flask_login import login_required
from application import app, db
from application.tasks.models import Task
from application.tasks.forms import TaskForm
@app.route("/tasks/", methods=["GET"])
def tasks_index():
return render_template("tasks/list.html", tasks = Task.query.all())
@app.route("/tasks/new/")
@login_required
def tasks_form():
return render_template("tasks/new.html", form = TaskForm())
Sovelluksiin halutaan usein erilaisia käyttäjärooleja. Jotkut resurssit ovat kaikkien käytettävissä, jotkut resurssit kaikkien kirjautuneiden käyttäjien näkyvissä, ja jotkut resurssit taas vain tiettyjen roolien kuten vaikkapa ylläpitäjän käytettävissä.
Hahmotellaan tässä toiminnallisuutta roolien lisäämiseen käyttäjälle. Muokataan ensin auth
-kansion models.py
-tiedostoa, ja lisätään käyttäjälle roles
-metodi. Metodi palauttaa listan käyttäjään liittyvistä rooleista.
from application import db
from application.models import Base
from sqlalchemy.sql import text
class User(Base):
__tablename__ = "account"
name = db.Column(db.String(144), nullable=False)
username = db.Column(db.String(144), nullable=False)
password = db.Column(db.String(144), nullable=False)
tasks = db.relationship("Task", backref='account', lazy=True)
def __init__(self, name):
self.name = name
def get_id(self):
return self.id
def is_active(self):
return True
def is_anonymous(self):
return False
def is_authenticated(self):
return True
def roles(self):
return ["ADMIN"]
# ...
Yllä olevassa esimerkissä kaikilla käyttäjillä on rooli "ADMIN". Todellisuudessa roolit kannattanee tallentaa tietokantaan erillisenä tauluna...
Olemassaoleva flask-loginin tarjoama @login_required
-dekoraattori ei tarjoa suoraan tukea roolien lisäämiseen. StackOverflow:sta löytyy kuitenkin aiheeseen liittyvä keskustelu, joka antaa osviittaa oman login_required
-dekoraattorin luomiseen.
Lisätään sovelluksen application
-kansion __init__.py
-tiedostoon rooleja tukeva login_required
-dekoraattori keskustelun esimerkin perusteella. Mikäli käyttäjää ei ole tai käyttäjä ei ole kirjautunut, hänellä ei ole pääsyä resurssiin. Mikäli käyttäjä on kirjautunut ja dekoraattoriin on määritelty tietty rooli, tarkastetaan onko käyttäjälle määritelty kyseistä roolia. Mikäli ei, käyttäjällä ei ole pääsyä resurssiin.
# roles in login_required
from functools import wraps
def login_required(role="ANY"):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user:
return login_manager.unauthorized()
if not current_user.is_authenticated():
return login_manager.unauthorized()
unauthorized = False
if role != "ANY":
unauthorized = True
for user_role in current_user.roles():
if user_role == role:
unauthorized = False
break
if unauthorized:
return login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
Tässä kohtaa on hyvä todeta Pythonin import-komennoista ja suoritusjärjestyksestä. Mikäli ylläolevaa haluaa käyttää osana omia näkymiä, tulee se määritellä __init__.py
:ssä ennen näkymien sovellukseen tuomista. Muulloin muodostuu tilanne, missä se ei ole märitelty kun sitä yritetään jo käyttää näkyvissä. Eräs mahdollinen ja toimiva järjestys on seuraava.
from flask import Flask
app = Flask(__name__)
# database connectivity and ORM
from flask_sqlalchemy import SQLAlchemy
import os
if os.environ.get("HEROKU"):
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL")
else:
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///tasks.db"
app.config["SQLALCHEMY_ECHO"] = True
db = SQLAlchemy(app)
# login functionality
from os import urandom
app.config["SECRET_KEY"] = urandom(32)
from flask_login import LoginManager, current_user
login_manager = LoginManager()
login_manager.setup_app(app)
login_manager.login_view = "auth_login"
login_manager.login_message = "Please login to use this functionality."
# roles in login_required
from functools import wraps
def login_required(role="ANY"):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user:
return login_manager.unauthorized()
if not current_user.is_authenticated():
return login_manager.unauthorized()
unauthorized = False
if role != "ANY":
unauthorized = True
for user_role in current_user.roles():
if user_role == role:
unauthorized = False
break
if unauthorized:
return login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
# load application content
from application import views
from application.tasks import models
from application.tasks import views
from application.auth import models
from application.auth import views
# login functionality, part 2
from application.auth.models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(user_id)
# database creation
try:
db.create_all()
except:
pass
Nyt sovellukseen voi määritellä roolikohtaista autorisaatiota. Alla esimerkki, missä tehtävän luominen vaatii roolin "ADMIN". Huomaathan myös, että login_required
ladataan sovelluksestamme flask_login
-kirjaston sijaan.
from flask import render_template, request, redirect, url_for
from flask_login import current_user
from application import app, db, login_required
from application.tasks.models import Task
from application.tasks.forms import TaskForm
# ...
@app.route("/tasks/", methods=["POST"])
@login_required(role="ADMIN")
def tasks_create():
form = TaskForm(request.form)
if not form.validate():
return render_template("tasks/new.html", form = form)
t = Task(form.name.data)
t.done = form.done.data
t.account_id = current_user.id
db.session().add(t)
db.session().commit()
return redirect(url_for("tasks_index"))
Roolit mahdollistavat ryhmäkohtaisen toiminnallisuuden rajaamisen. Mikäli sovelluksen toimintaa haluaa rajata niin, että resursseihin pääsy (esimerkiksi muokkaus) on käyttäjäkohtaista, tulee osa autorisointitoiminnallisuudesta lisätä näkymien metodeihin. Voisimme esimerkiksi haluta, että vain tehtävän luonut käyttäjä voi asettaa tehtävän tehdyksi. Tämä onnistuu seuraavalla tavalla.
from flask import render_template, request, redirect, url_for
from flask_login import current_user
from application import app, db, login_manager, login_required
from application.tasks.models import Task
from application.tasks.forms import TaskForm
@app.route("/tasks/<task_id>/", methods=["POST"])
@login_required
def tasks_set_done(task_id):
t = Task.query.get(task_id)
if t.account_id != current_user.id:
# tee jotain, esim.
return login_manager.unauthorized()
t.done = True
db.session().commit()
return redirect(url_for("tasks_index"))
Kun sovellukseen lisää autorisointitoiminnallisuutta, kannattaa sovellusta muokata myös niin, että käyttöliittymä ei näytä kiellettyjä toiminnallisuuksia. Kolmannen viikon materiaalissa on esimerkki kirjautuneen käyttäjän nimen näyttämiseen -- samaa esimerkkiä soveltaen voi rajata resursseja myös esimerkiksi roolien perusteella.
Flaskia varten löytyy merkittävä joukko tietoturvakirjastoja, joista osa tarjoaa roolien hallintaa "out of the box"-toiminnallisuutena. Eräs tällainen on Flask Security.
Käytettävyys
Harjoitustyön tulee olla käytettävä sekä saavutettava. Tässä aiheeseen liittyviä hyviä linkkejä.
- Sari A. Laakson materiaali simulointipohjaisesta käyttöliittymäsuunnittelusta.
- Tutkimuspohjaista tietoa käytettävyydestä.
- material.io: Opas saavutettavuuteen.
- Ronja Ojan Verkkopalveluiden saavutettavuus -kurssin materiaali.
- Työvälineitä verkkosivujen saavutettavuuden tarkasteluun.
Käy edelliset materiaalit läpi. Kiinnitä huomiota erityisesti seuraaviin asioihin (Sari A. Laakson sekä Ronja Ojan luentomateriaaleista)
- Hyödyllisyys: voiko järjestelmällä tehdä toivotut asiat (eli onnistuuko käyttötapausten tekeminen tai voidaanko sillä tehdä user storyjen määrittelemät toiminnot).
- Tehokkuus: voidaanko toivotut asiat tehdä ilman turhia välivaiheita ja turhaa kognitiivista työtä.
- Opittavuus: keksiikö käyttäjä, joka haluaa suorittaa tietyn tehtävän, mitä pitäisi tehdä ja miten.
- Muistettavuus: kun käyttäjä saa asian tehtyä, muistaako hän miten asia tehdään jatkossa.
- Tyytyväisyys: onko sovelluksen käyttäminen mielekästä.
- Virhealuttius: ohjaako sovellus virheellisiin toimintoihin ja miten käyttäjä selviytyy näistä toiminnoista.
- Johdonmukaisuus: sivun HTML-elementtejä käytetään johdonmukaisesti niiden oikeisiin tarkoituksiin (listaa käytetään listana, taulukkoa taulukkona, otsikkoa otsikkona, otsikkojen tasot kuvaavat niiden merkitystä). Näiden lisäksi tekstikenttiin liittyvät label-kentät ovat kuvaavat, ja niihin liittyvät label-for -metatiedot ovat oikein. Kts. erityisesti Ronja Ojan "Luento 2: Perus HTML:ää saavutettavasti"
Tarkastele sovelluksesi saavutettavuutta myös AChecker-palvelulla.