Osan 5 etapit
  • Autorisointi.
  • Käytettävyyden viilausta.
  • Toiminnallisuuden täydentäminen (uusia ominaisuuksia).
  • Kirjoita työhösi alustava asennusohje ja käyttöohje.

Tietokantasovellus-kurssin viidennen osan materiaali sisältää linkkejä autorisointiin ja käytettävyyteen (mukaanlukien saavutettavuus) liittyen. Viidennen osan 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 muokaten keskustelun esimerkkiä edelleen. 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(_func=None, *, role="ANY"):
    def wrapper(func):
        @wraps(func)
        def decorated_view(*args, **kwargs):
            if not (current_user and current_user.is_authenticated):
                return login_manager.unauthorized()

            acceptable_roles = set(("ANY", *current_user.roles()))

            if role not in acceptable_roles:
                return login_manager.unauthorized()

            return func(*args, **kwargs)
        return decorated_view
    return wrapper if _func is None else wrapper(_func)

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(_func=None, *, role="ANY"):
    def wrapper(func):
        @wraps(func)
        def decorated_view(*args, **kwargs):
            if not (current_user and current_user.is_authenticated):
                return login_manager.unauthorized()

            acceptable_roles = set(("ANY", *current_user.roles()))

            if role not in acceptable_roles:
                return login_manager.unauthorized()

            return func(*args, **kwargs)
        return decorated_view
    return wrapper if _func is None else wrapper(_func)


# 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 osan materiaalissa on esimerkki kirjautuneen käyttäjän nimen näyttämiseen -- samaa esimerkkiä soveltaen voi rajata resursseja myös esimerkiksi roolien perusteella.

Flask Security

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ä.

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.

Sisällysluettelo