HOME Python

デコレータを用いた bottle.py のアクセスコントロール


1. 初めに

bottle.py は軽量な python 製 web フレームワークです。 機能はルーティングとレンダリングだけの簡単なフレームワークで、極めて簡単に習得できます。 そのため、モックアップの作成に最適で、紫藤もしばしば利用しています。

bottle.py には セッション管理機能はないのですが、beaker と組み合わせてセッション管理をすることができます。 この記事ではセッションを用いた実用的なアクセスコントロールについて述べます。

2. インストールと基本手な使いかた

bottle.py の基本的な使い方は、 Bottleチュートリアル(日本語訳) などを参考にしてください。

以下、簡単に記載します。

2.1. インストール

bottle.py や beaker のインストール方法は ここここに詳しく書いてあります。 pip を使えば簡単に、
pip install bottle
pip install beaker
でインストールできます。

2.2. bottle.py の基本的な使い方

Bottleチュートリアル(日本語訳) Bottleのチュートリアル:To-Do - リスト·アプリケーション を見てください。

2.3. beaker.middleware.SessionMiddreware と bottle との連携

bottle.py でセッション管理を行うためには、beaker と連携させる必要があります。 詳しくは、 Recipes — Bottle 0.13-dev documentation を見てください。

3. bottle.py と beaker を使ったアクセスコントロール

3.1. 方針

ユーザに権限 (role) を持たせて、その権限によってアクセスコントロールします。 たとえば、以下のようなアクセスコントロールを考えます。

3.1. 準備

上の方針を実現するために、users テーブルをデータベースに作成します。 users テーブルには uid, pw, role の 3 つのフィールドがあり、uid が主キーです。

sqlite での create table 文は以下のようになります。

create table users(
  uid text primary key not null,
  pw text not null,
  role text not null
)

3.2. 素直な実装

以下に素直な実装を示します。 権限があればそのページを表示し、なければログインページにリダイレクトします。

3.2.1 ログイン画面

ログインに成功するとセッション情報に uid と role が保持されます。 form_get(), session_get(), session_set(), get_role() の機能は以下の通りです。 また、bottle は btl として import しています。

関数名 機能
form_get(k) POST メソッドで渡されたフォームから k の値を取り出す
session_get(k, delete=False) セッションから k の値を取り出す。delete == True のときは取りだしたあと削除する。
session_set(k, v) セッションの項目 k に v を保持する
get_role(uid, pw) DB に問い合わせて、uid, pw をもつユーザの role を返す。存在しない場合には None を返す。

001:   @btl.get('/login')
002:   @btl.view('login')
003:   def login_form():
004:       return {'message':session_get('message', True)}
005:   
006:   
007:   @btl.post('/login')
008:   @btl.view('login')
009:   def login():
010:       uid=form_get('uid')
011:       pw=form_get('pw')
012:       role=get_role(uid, pw)
013:       if role is not None:
014:           session_set('uid', uid)
015:           session_set('role', role)
016:           session_set('message', 'こんにちは、{} さん'.format(uid))
017:           return btl.redirect('/')
018:       else:
019:           return {'message':'ID か Password が間違っています。'}
020:           
021:   
022:   
023:   @btl.route('/logout')
024:   def logout():
025:       s = btl.request.environ['beaker.session']
026:       s.delete()
027:       return btl.redirect('/login')

3.2.2 TOP 画面

ログインしていれば TOP 画面にアクセスすることができます。 セッション情報の role が None でなければ ログインしていると判断します。 ログインしてない場合はログイン画面に遷移します。
001:   @btl.route('/')
002:   @btl.view('index')
003:   def index():
004:       '''TOP page'''
005:       role=session_get('role')
006:       if role is not None:
007:           return {'message':session_get('message', True),
008:                   'role':role}
009:       else:
010:           session_set('message', 'ログインが必要です')
011:           return btl.redirect('/login')

3.2.3 ユーザ一覧画面

admin ユーザはユーザ一覧画面にアクセスすることができます。 セッション情報 role の値が 'admin' ならユーザ一覧を表示し、 そうでない場合はログイン画面に遷移します。
001:   @btl.route('/user')
002:   @btl.view('users')
003:   def users():
004:       '''List of users'''
005:       role=session_get('role')
006:       if role=='admin':
007:           return {'users':users()}
008:       else:
009:           session_set('message', 'admin でログインが必要です')
010:           return btl.redirect('/login')

3.2.4. この実装で分かったこと

beaker と組み合わせれば、bottle.py でもアクセスコントロールができることがわかりました。

ただ、上のコードを見るとわかるように、アクセス制御のために同じような処理が index() と users() に書かれています。 実際のサイトでは、この処理を多くの関数に書く必要があるので、いちいちコーディングしていくのは非効率です。

そこで、次の節でデコレータを使ってコードを短くすることを考えます。

3.3. デコレータを使った実装

アクセス制御を抽象化して外出しするために、デコレータを使うことを考えます。 たとえば、TOP 画面のようにログインが必要な画面では @req_login、 ユーザ一覧のように admin 権限が必要な画面では @req_admin というデコレータを使えば、アクセス制御できるようにします。

アクセス制御のためのデコレータを作るクラスを Auth とします (実装は 3.4 で示します)。 まず、Auth.config() でデータベースにアクセスして role を取得する関数を設定し、 それから、デコレータ (ここでは req_admin, req_login) を作成します。 Auth.config() では次の7つの関数を設定できます。そのうち、DB にアクセスして role を取得するメソッド (get_role_from_db) は必ず設定する必要があります。

デコレータを定義するときは、role, 権限がない時に表示されるエラーメッセージ, 権限がない時のリダイレクト先を指定します。

001:   Auth.config(get_role_from_db=ut.get_role)
002:   
003:   # decorators for access controll
004:   req_admin=Auth(role='admin', message='Only admin users can access this page.')
005:   req_login=Auth()

Auth.config() のキーワードパラメター
メソッド名 機能
get_role_from_db uid, pw を引数にとり、DB に保持されている role を返す。必須
logout セッション情報をクリアする。省略可
get_role セッションから role の情報を取得する。省略可
get_uid セッションから uid の情報を取得する。省略可
set_role セッションに role を設定する。省略可
set_uid セッションに uid を設定する。省略可
set_message セッションにメッセージを設定する。省略可

デコレータ作成時のキーワードパラメター
パラメータ名 デフォルト 説明
role None 権限
message 'login required.' 権限がない時にセッション変数 'message' に設定される文字列
failure_redirect '/login' 権限がない時のリダイレクト先

アクセス制御をデコレータで外出しにすると、TOP 画面とユーザ一覧画面の関数は以下のようになります。 関数本体は1行でかけてしまいます。 デコレータの順番は、上からルーティング、アクセス制御、レンダリングです。この順番で書かないと動きません。

001:   @btl.route('/')
002:   @req_login
003:   @btl.view('index')
004:   def index():
005:       return {'message':ut.session_get('message', True),
006:               'role':Auth.get_role()}
007:   
008:   
009:   @btl.route('/user')
010:   @req_admin
011:   @btl.view('users')
012:   def users():
013:       return {'users':ut.users()}
また、ログイン、ログアウト画面は以下のようになります。
001:   @btl.post('/login')
002:   @btl.view('login')
003:   def login():
004:       uid,pw = [ut.form_get(x) for x in ('uid', 'pw')]
005:       if Auth.login(uid,pw):
006:           ut.session_set('message', 'こんにちは、{} さん'.format(uid))
007:           return btl.redirect('/')
008:       else:
009:           return {'message':'ID か Password が間違っています。'}
010:   
011:   
012:   @btl.route('/logout')
013:   def logout():
014:       Auth.logout()
015:       return btl.redirect('/login')

3.4. Auth の実装

Auth のソースコードは以下のようになります。50 行未満の短いコードです。

このモジュールは、uid と pw を引数に与えて DB から role を取得できる関数があれば、バックエンドの DB の種類にかかわらず使うことができます。 ここでは sqlite を使いましたが、他の RDBM でもよいし、Key-Value 型のストレージも使えます。

39--48 行目の __call__() がデコレータとして呼ばれたときの挙動で、権限があるときに引数で与えられた関数を呼び出し、 そうでなければ、/login に遷移する関数を返します。

001:   import bottle as btl
002:   import session_utils as ut
003:   from functools import partial, wraps
004:   
005:   class Auth:
006:       '''generating decorators for access control'''
007:          
008:       # class attribute
009:       CLS_ATTR=[
010:         ('get_role_from_db', lambda uid,pw:None), # lambda uid,pw: role if a record having (uid, pw) exists in the db else None 
011:         ('logout', ut.logout), # method to clear the session
012:         ('set_uid', partial(ut.session_set, 'uid')), # method to set uid into the session
013:         ('set_role', partial(ut.session_set, 'role')), # method to set role into the session
014:         ('get_uid', lambda:ut.session_get('uid')), # method to get uid from the session
015:         ('get_role', lambda:ut.session_get('role')), # method to get role from the session
016:         ('set_message', partial(ut.session_set,'message')), 
017:       ]
018:       
019:       @classmethod
020:       def config(cls, **kw):
021:           '''setting static methods'''
022:           for k,v in cls.CLS_ATTR:
023:               setattr(cls, k, staticmethod(kw.get(k,v)))
024:               
025:       @classmethod    
026:       def login(cls, uid, pw):
027:           role=cls.get_role_from_db(uid, pw)
028:           if role:
029:               cls.set_uid(uid)
030:               cls.set_role(role)
031:           return role
032:           
033:       def  __init__(self,**kw):
034:           '''kw parameters are role, message and failure_redirect'''
035:           for k,v in [('message', 'Login required'), ('failure_redirect', '/login')]:
036:               setattr(self, k, kw.get(k, v))
037:           self.is_auth=(lambda :kw['role']==self.get_role()) if 'role' in kw else self.get_role
038:                               
039:       def __call__(self, fun):
040:           '''acting as a decorator'''
041:           @wraps(fun)
042:           def _f(*a, **k):
043:               if self.is_auth():
044:                   return fun(*a, **k)
045:               else:
046:                   self.set_message(self.message)
047:                   return btl.redirect(self.failure_redirect)
048:           return _f

4. サンプルコード

セッション管理の実際のコードをアップしておきます。興味のある人はダウンロードして動かしてみてください。

4.1. ダウンロード

ここからサンプルコードをダウンロードできます。

4.2. 使い方

  1. 前提: python 3.x, bottle, beaker がインストールされている。
  2. 解凍: ダウンロードした bottle_session.zip を解凍する。
  3. DB 作成: 解凍して生成したディレクトリ bottle_session にいって、python init.py と入力する。DB が初期化される。
  4. 実行: 続けて python session.py と入力する。アプリケーションサーバが立ち上がる。
  5. アクセス: ブラウザで http://127.0.0.1:8080/login にアクセスする。初期ユーザは以下の通り。
初期ユーザ
uid pw role
john smith guest
peter norvig admin

4. 終わりに

python のデコレータはとても強力な機能で、ここで示したように共通で使われているルーチンを外だしすることができます。 その結果、ソースコードが短くなり、可読性も大幅に増加します。

実は bottle.py にはアクセス制御用の plug-in があるのですが、 自分で書いても大した手間でないのと、自作のものの方が、自分で使う分には便利なので、書いてみました。

bottle.py は徐々に発展しており、便利な plug-in も増えてきているので、 小規模なサイトであれば業務用にも使えるようになってきています。