Pyramidのセキュリティ

始めに断っておこう。Pyramidにはファンシーなログインフォームやユーザー管理なんてついてこない。 認証、認可の仕組みはあるが、Pyramidに設定一発で動くような押しつけがましいViewやModelは存在しない。 そういうのが好きな人はDjangoというフレームワークがあるから、そっちにしときな。

このエントリでは、Pyramidで認証、認可の仕組みを使う方法を説明する。 CSRFとかそういうのは扱わないのであしからず。

Pyramidのセキュリティの仕組み

先に述べたとおりPyramidには認証と認可の仕組みがある。 認証というのは、今アプリケーションを使っているのが誰なのかを特定するもので、 認可は誰がその機能や処理を実行してよいかということである。

認証

Pyramidの認証では、AuthenticationPolicyがリクエストからprincipalを取り出すという方法で実現している。 princpalというのはユーザーがシステムの中でどのように認識されているかということであり、 例えば単純にログインユーザーで判定されることもあるし、特定のIPアドレスやネットワークからアクセスしているといった情報から判定されることもある。

Pyramidで標準で用意されているAuthenticationPolicyでわかりやすいのはBasicAuthAuthenticationPolicyだろう。 このポリシーが設定されていると、リクエストのAuthorizationヘッダの情報からprincipalを生成する。 このほかに、セッションやクッキーなどで認証情報を保持するポリシーが用意されている。

結局のところ用意されているのは、リクエストからprincipalを取り出す方法だけで、実際にそのprincipalを有効なものだと判定するのは、アプリケーション側にゆだねられている。 有効なprincipalと判定した後に、rememberメソッドによってAuthenticationPolicyに渡すと、それ以降のPyramidのセキュリティメカニズムの中で利用されるというわけだ。

認可

さて、利用者の役割などがわかれば、次はその人に何が許されているかという話になる。 Pyramidでは、許されていること(権限)をpermissionという用語で扱っている。 permissionはPyramidの実装上は単なる文字列である。(ただし、ドキュメントで説明されていないが、実際にはタプルなども利用可能である。今のところ。)

実際に認可が必要になるのはビューを実行するときである。 ビューはadd_viewやview_configで登録するときにpermissionも指定できる。 ビューを実行しようとしたときに指定したpermissionを持っていなければHttpForbiddenExceptionが発生して、例外ビューの処理に飛ばされてしまう。

このpermissionはprincipalに直接結びついているわけではない。 permissionとprincipalを結びつけるのがAuthorizationPolicyの役割である。

ここで、Pyramid標準で唯一用意されているACLAuthorizationPolicyの動きを見てみよう。

まず、なんらかの認証により、"aodag", "authenticated", "group:staff" という3つのprincipalを得ているとしよう。 ここで、あるURLにアクセスするが、ACLAuthorizationPolicyは、コンテキストの__acl__プロパティを使って、permissionを取得する。

from pyramid.security import Allow
class SecurityContext(object):
    __acl__ = [
        (Allow, "authenticated", "view"),
        (Allow, "group:staff", "edit"),
    ]
    def __init__(self, request):
        self.request = request

上記のコンテキストによって、 "authenticated" から "view"パーミッション、 "group:staff"から"edit"パーミッションを得られる。 ではこの状態で以下のようなビューがあるとしよう。

@view_config(context=SecurityContext, permission="view")
def protected_view(request):
    return dict()

@view_config(context=SecurityContext, permission="edit")
def edit_view(request):
    return dict()

@view_config(context=SecurityContext, permission="add_user")
def add_user_view(request):
    return dict()

二種類のビューそれぞれに異なるパーミッションが設定されている。 今アクセスしているユーザーは "view","edit"の2つのパーミッションを持っているため、 protected_view, edit_viewにアクセスできる。 しかし、"add_user" パーミッションは持っていないため、add_user_viewへのアクセスはできない。

では、パーミッションが要求されたときの流れをまとめてみよう。

  1. AuthenticationPolicyがリクエストからprincipalを取り出す
  2. AuthorizationPolicyがコンテキストからprincipalにマッチするpermissionを取り出す
  3. 上記で取り出されたpermission中にviewで指定されたpermissionが含まれているか判定する

Pyramidが用意しているのはこの流れだけであり、AuthenticationPolicyやAuthorizationPolicyは標準で用意されているものや、自分でカスタマイズしたものが使えるようになっている。

アプリケーションでの実際

さて、Pyramidが提供する認証、認可の仕組みを理解したところで、実際のアプリケーションでどのように利用するか考えてみよう。

セキュリティ設定

認証、認可ともに、Configuratorでの設定によって有効になる。

config.set_authetication_policy(AuthTktAuthenticationPolicy(secret="fieaoji3w-bb#"))
config.set_authorization_policy(ACLAuthorizationPolicy())

AuthTktは、クッキーに認証トークンを持たせる方法の認証ポリシーである。 Webアプリケーションサーバーを分散している場合でも、セッションレプリケーションなどを必要とせずに認証情報を扱える。 ACLAuthorizationPolicyはすでに説明したとおり、標準ではこれしか提供されていない。

ビューとモデル

例として、ありがちなWikiアプリケーションに以下のようなセキュリティモデルを設定してみよう。

  • WikiPageは 誰でも 読むこと(view)ができる
  • WikiPageは 認証済のユーザー だけ作成(create)できる
  • WikiPageは 作成者 によってロック(lock) することができる
  • WikiPageは 作成者 によってロック解除(unlock) することができる
  • ロックされたWikiPageは 作成者 のみが編集(edit)できる
  • ロックされていないWikiPageは 認証済のユーザー が編集(edit)できる

これらを __acl__ に定義したモデル、WikiPage:

class WikiPage(Persistent):
    def __init__(self, pagename, data, owner):
        self.__name__ = pagename
        self.pagename = pagename
        self.data = data
        self.owner = onwer
        self.locked = False

    @property
    def __acl__(self):
        if self.locked:
            return [(Allow, Everyone, "view"),
                    (Allow, self.owner, "unlock"),
                    (Allow, self.owner, "edit")]
        else:
            return [(Allow, Everyone, "view"),
                    (Allow, self.owner, "lock"),
                    (Allow, Authenticated, "edit")]

    def lock(self):
        self.locked = True

    def unlock(self):
        self.locked = False


class Wiki(PersistentMapping):
    @property
    def __acl__(self):
        return [(Allow, Authenticated, "create")
        ]

上記のようにコンテキストによって、権限が与えられる。 では、これらの権限を要求するビューの定義をしていこう。

@view_config(context=WikiPage, permission="view", renderer="wikipage.pt")
def wikipage_view(context, request):
    return dict(wikipage=context)


@view_config(context=WikiPage, name="lock", permission="lock", request_method="POST")
def wikipage(context, request):
    context.lock()
    return HTTPFound(location=request.resource_url(context))

@view_config(context=WikiPage, name="unlock", permission="unlock", request_method="POST")
def wikipage(context, request):
    context.unlock()
    return HTTPFound(location=request.resource_url(context))

@view_config(context=WikiPage, name="edit", permission="edit", renderer="wikipage_edit.pt")
def wikipage_edit(context, request):
    form = Form(context)
    if request.method == "POST":
        if form.validate(request.POST):
            context.data = request.POST['data']
            return HTTPFound(location=request.resource_url(context)
    return dict(wikipage=context, form=form)

@view_config(context=Wiki, name="create", permission="create", renderer="wikipage_create.pt")
def wikipage_create(context, request):
    form = Form()
    pagename = request.matchdict['subpath']
    if request.method == "POST":
        if form.validate(request.POST):
            page = WikiPage(data=request.POST['data'], pagename=pagename, owner=authenticated_userid(request))
            page.__parent__ = context
            context[page.__name__] = page
            return HTTPFound(location=request.resource_url(page)
    return dict(wiki=context, form=form)

このように、ビュー内では権限などを気にすることなく本来の機能的な処理だけを記述できる。 ログイン中のユーザーは authenticated_userid で取得できる。

ログイン処理

さて、ここまで話を避けていた感のあるログイン処理である。 認証というとログイン処理のことであろうと思われるかもしれないが、前述のとおりログイン処理はpyramidで提供していないため、アプリケーションで用意しなければならない。

まずは、ユーザーデータである。

import hashlib

class User(Persistent):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def set_password(self, password):
        self._password = hashlib.sha1(password.encode('utf-8')).hexdigest()

    password = property(fset=set_password)

    def verify_password(self, password):
        return self._password == hashlib.sha1(password.encode('utf-8')).hexdigest()

パスワードをハッシュ化して保存する処理があるため分かりにくいが、やれることは usernameに対するpasswordが正しいかどうかだけである。 このUserモデルを使ったlogin APIを作る:

def get_user(request, username):
    conn = get_connection(request):
    root = conn.root()
    users = root['users']
    return users.get(username)


def login(request, username, password):
    user = get_user(request)

    if user is None:
        return None

    if not user.verify_password(password):
        return None

    return user

ログインフォームではこのAPIを利用する:

def login_form(request):
    if request.POST:
        user = login(request, request.POST.get('username'), request.POST.get('password'))
        if user is not None:
            headers = remember(request, user.username)
            return HTTPFound(request.resource_url(request.root), headers=headers)
    return dict()

rememberは、クッキーやAuthorizationヘッダなど、レスポンスに必要な情報を返してくる。 (認証の種類によっては、ヘッダ情報を使わずにセッションなどに追加する場合もあるが、その場合は空のリストが返ってくる。) ログインに成功した場合はm、rememberの結果をヘッダに追加したレスポンスを返すようにする。

ログアウトは単純にforgetを呼び、戻り値をレスポンスヘッダに追加する:

def logout(request):
    headers = forget(request)
    return HTTPFound(request.resource_url(request.root), headers=headers)

まとめ

  • Pyramidは認証、認可の仕組みを持っているが、具体的に即使えるような出来合いのものではない
  • 認証はリクエストにどのように認証情報が入ってるのかだけに対応している
  • 認可は認証されたprincipalに対して、現在のコンテキストにどのような権限を与えるかというもの

ヽ(´_・ω・)_この内容は、結構しんどかった。ちなみに前回のエントリでシュークリームを2つほどせしめたので、次はミルクティーとかいいなって思った(小並

Comments !

blogroll

social