Play2-authを使ってみる - 認可/Authorization編


## 概要

前回の記事 ([Play2-authを使ってみる - 認証/Authentication編](http://qiita.com/uryyyyyyy/items/37788ff980268558c1cc))では認証・認可のざっくりした話とplay2-authと使って認証まで試してみました。
この記事では次の認可をやってみます。

（ここでは、ユーザーに既に付与されているはずの権限に対しての権限制御が上手く行っていることを確認します。）

また、せっかくなのでテストも書いてみましょう。

## この記事のゴール

- 認証・認可の大まかな流れを理解する
- play2-authで認可部分まで作り動作確認する
  - ログインしたユーザーIDが正しいことを確認
  - ユーザーIDに付与された権限が与えられていることの確認
- Play2-authを実装したControllerのテスト

## 環境

Play 2.5
play2-auth 0.14.2

[前回の記事](http://qiita.com/uryyyyyyy/items/37788ff980268558c1cc)の続きからになります。

また、stackable-controllerを用いているので、以下の記事を読んでおくと理解が早いかもしれません。
[Play2のstackable-controllerを使ってみる](http://qiita.com/uryyyyyyy/items/5fa520cd38574284f8c5)

全体のコードは以下。
https://github.com/uryyyyyyy/play2sample/tree/play2-auth-authorize


## 実装の流れ

- （ServiceをDIできるようにする）
- 認可用のコントローラを作る
- 権限制御を用意する
- 動作確認する
- テストを書く

という流れになります。

### ServiceをDIできるようにする

ここでのコードは本筋とそれるので省略します。

詳しくはコードとこちらの記事を見て下さい。
[Play2のGuice DIを使う＆テストも書く](http://qiita.com/uryyyyyyy/items/969acafb3b9b2f11bb60)

### 認可用のコントローラを作る

認可用のコントローラを作るには、AuthElement と前回作った AuthConfigImpl を継承する必要があります。

そして、Actionの代わりにStackAction（AsyncStack）を使うことで権限チェックが出来ます。

```scala:AuthorizeController.scala
import javax.inject.{Inject, Singleton}

import akka.actor.ActorSystem
import jp.t2v.lab.play2.auth.AuthElement
import play.api.mvc.{Controller, Result}
import utils.{Administrator, AuthService, MyUser, NormalUser}

import scala.concurrent.{ExecutionContext, Future}

@Singleton
class AuthorizeController @Inject() (actorSystem: ActorSystem,
  val authService: AuthService) extends Controller with AuthElement with AuthConfigImpl {

  implicit val myExecutionContext: ExecutionContext = actorSystem.dispatcher

  def checkAdminRole = AsyncStack(AuthorityKey -> Administrator) { implicit request =>
    val user = loggedIn
    returnUser(user)
  }

  def returnUser(user:MyUser): Future[Result] ={
    Future(Ok("id: " + user.id))
  }

  def checkNormalRole = AsyncStack(AuthorityKey -> NormalUser) { implicit request =>
    val user = loggedIn
    Future(Ok("id: " + user.id))
  }

}
```

`AsyncStack(AuthorityKey -> Administrator)`の箇所で、このAPIにはAdministrator権限が必要なことを示しています。
同様に`AuthorityKey -> NormalUser`のAPIはNormal権限を持っていることになります。

また、AuthElementに付いている`loggedIn`というメソッドを呼び出すことでログイン済みのユーザー情報を取得することが出来ます。この機能はstackable-controllerによって提供されています。
この機能を使って、今ログインしているユーザーを確認してみることも一緒にやってみます。
（ちなみに AuthElement 以外にもOptionを返すelementもついてたりするので、必要に応じて使い分けられます。）

また、routesに以下を追加しましょう。
後ほど、このAPIを叩いてみて権限制御されていることをチェックします。

```
GET     /authorize/admin              controllers.auth.AuthorizeController.checkAdminRole
GET     /authorize/normal              controllers.auth.AuthorizeController.checkNormalRole
```

### 権限制御を用意する

さて、上記だけでも動作するのですが、前回の記事で`authorize`メソッドが常にtrueを返すようにしているため、どの権限のユーザーでも全ての操作が出来てしまいます。

そこで、

- Admin権限の操作はAdminユーザーのみができる
- Normal権限の操作はAdmin/Normalのユーザーができる

というようにauthorizeメソッドの挙動を変えてみます。

```scala:AuthConfigImpl.scala
  override def authorize(user: User, authority: Authority)(implicit context: ExecutionContext): Future[Boolean] = {
    Future.successful(
      (user.role, authority) match {
        case (Administrator, _)       => true // AdminならどんなActionでも全権限を開放
        case (NormalUser, NormalUser) => true // ユーザがNormalUserで、ActionがNormalUserなら権限あり。もしActionがAdminだけなら権限なしになる。
        case _                        => false
      }
    )
  }
```

tupleの左がユーザーの権限、右が操作に対しての権限と理解しています。
（右と左の型を変えることもできそう？）

### 動作確認する

cookieが有効になっているブラウザで試して下さい。

#### Normalユーザーでログイン

まず、認証されていないとAPIを叩けないことを確認します。
念のためcookieをresetした上で、以下を叩きます。

`http://localhost:9000/authorize/normal`

まだ認証が済んでいないため、`authenticationFailed`にて定義されている「Unauthorized」が返ってきます。

さて、以下を叩いてログインを行います。

`http://localhost:9000/authentication/login/normal/pass2`

「login success」が返ると、normalユーザーでログインできたことを意味します。

次に、もう一度以下のAPIにアクセスします。

`http://localhost:9000/authorize/normal`

すると、「id: normal」が返ります。これは、このnormal権限のAPIへのアクセスが成功して、またログインしているユーザーIDがnormalであることを意味しています。

次に、Admin権限が必要なAPIを叩いてみます。

`http://localhost:9000/authorize/admin`

すると、`authorize`メソッドでの権限チェックに失敗して、`authorizationFailed`で定義された「No permission」が返ってきます。

normal権限のユーザーではAdmin権限のAPIを叩けないことがわかりました。


#### Adminユーザーでログイン

まず、認証されていないとAPIを叩けないことを確認します。
念のためcookieをresetした上で、以下を叩きます。

`http://localhost:9000/authorize/normal`

まだ認証が済んでいないため、`authenticationFailed`にて定義されている「Unauthorized」が返ってきます。

さて、以下を叩いてログインを行います。

`http://localhost:9000/authentication/login/admin/pass1`

「login success」が返ると、adminユーザーでログインできたことを意味します。

次に、もう一度以下のAPIにアクセスします。

`http://localhost:9000/authorize/normal`

すると、「id: admin」が返ります。これは、このnormal権限のAPIへのアクセスが成功して、またログインしているユーザーIDがadminであることを意味しています。

次に、Admin権限が必要なAPIを叩いてみます。

`http://localhost:9000/authorize/admin`

今度はAPIを叩くことに成功して、再び「id: admin」が返ります。
先ほどは権限がなかったAdmin権限のAPIを叩けるようになっていることが確認できました。

### テストを書く

最後にテストを書きます。

sontrollerのテストであれば、E2Eというか入り口から全部チェックすべきなのですが、

- play2-authがplay Applicationに依存してる部分があってテストしにくい
- 今回はidContainerやtokenAccessorをDIできていない

ので、全体のテストは諦めることにします。
（うまくDIできれば、controllerのコンストラクタDIでMockを挟むことでテストしやすくなるかもしれません。）

ここでは、権限チェックを通過した後のメソッド `returnUser` だけテストします。

```scala:AuthorizeControllerTest
import java.util.concurrent.TimeUnit

import akka.actor.ActorSystem
import akka.util.Timeout
import org.scalatest.mock.MockitoSugar
import org.scalatest.{FunSpec, MustMatchers}
import play.api.test.Helpers
import utils.{Administrator, MyUser}

class AuthorizeControllerTest extends FunSpec with MustMatchers with MockitoSugar  {

  describe("AuthorizeControllerTest") {

    implicit val timeout = Timeout(5000, TimeUnit.MILLISECONDS)

    it("returnUser"){

      val actorSystem = ActorSystem.apply()
      val controller = new AuthorizeController(actorSystem, null)
      val user = MyUser("admin", "pass", Administrator)
      val result = controller.returnUser(user)

      Helpers.contentAsString(result) mustBe "id: admin"
      Helpers.status(result) mustBe 200
    }
  }
}
```

見て分かるとおり、普通のcontrollerのテストと同じようにできます（当たり前か。。）

## まとめ

play2-authを使って、認証と認可（権限チェック）を見てみました。
少々長くなってとっちらかりましたが、そもそもの仕組みが難しいわりには理解しやすい実装をできているなと感じました。