概要
前回の記事 (Play2-authを使ってみる - 認証/Authentication編)では認証・認可のざっくりした話とplay2-authと使って認証まで試してみました。
この記事では次の認可をやってみます。
(ここでは、ユーザーに既に付与されているはずの権限に対しての権限制御が上手く行っていることを確認します。)
また、せっかくなのでテストも書いてみましょう。
この記事のゴール
- 認証・認可の大まかな流れを理解する
- play2-authで認可部分まで作り動作確認する
- ログインしたユーザーIDが正しいことを確認
- ユーザーIDに付与された権限が与えられていることの確認
- Play2-authを実装したControllerのテスト
環境
Play 2.5
play2-auth 0.14.2
前回の記事の続きからになります。
また、stackable-controllerを用いているので、以下の記事を読んでおくと理解が早いかもしれません。
Play2のstackable-controllerを使ってみる
全体のコードは以下。
https://github.com/uryyyyyyy/play2sample/tree/play2-auth-authorize
実装の流れ
- (ServiceをDIできるようにする)
- 認可用のコントローラを作る
- 権限制御を用意する
- 動作確認する
- テストを書く
という流れになります。
ServiceをDIできるようにする
ここでのコードは本筋とそれるので省略します。
詳しくはコードとこちらの記事を見て下さい。
Play2のGuice DIを使う&テストも書く
認可用のコントローラを作る
認可用のコントローラを作るには、AuthElement と前回作った AuthConfigImpl を継承する必要があります。
そして、Actionの代わりにStackAction(AsyncStack)を使うことで権限チェックが出来ます。
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メソッドの挙動を変えてみます。
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
だけテストします。
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を使って、認証と認可(権限チェック)を見てみました。
少々長くなってとっちらかりましたが、そもそもの仕組みが難しいわりには理解しやすい実装をできているなと感じました。