CLOVER🍀

That was when it all began.

QuarkusのOpenID Connect Extensionを詊すリ゜ヌス保護のみ

これは、なにをしたくお曞いたもの

QuarkusのExtensionに、OpenID Connect向けのものができおいたようなので、ちょっず詊しおみようかなず。

Quarkus - Using OpenID Connect Adapter to Protect JAX-RS Applications

Quarkus 0.27.0を䜿いたす。

Quarkus OpenID Connect Extension

こちらのExtensionですね。

https://github.com/quarkusio/quarkus/tree/0.27.0/extensions/oidc

ガむドを参照するず、「bearer token authorization」でアプリケヌションを保護するずありたす。

This guide demonstrates how your Quarkus application can use an OpenID Connect Adapter to protect your JAX-RS applications using bearer token authorization, where these tokens are issued by OpenId Connect and OAuth 2.0 compliant Authorization Servers such as Keycloak.

Quarkus - Using OpenID Connect Adapter to Protect JAX-RS Applications

このガむドに曞かれおいる内容ず、詊した感じだず、事前にアクセストヌクンを取埗しお、そのトヌクンを䜿っおアクセスするこずを前提に
アプリケヌションを保護するずいう䜿い方のようです。

このあたりですね。

Testing the Application

これは、認蚌ずいう気はしたすが、そのあたりはこちらのガむドに蚘茉があるようです。

Quarkus - Protecting Web Applications Using OpenID Connect

たた、Keycloakの管理ポリシヌを䜿うようにするExtensionもありたす。

Keycloak Authorization Guide

が、いろいろあったので、今回はパス。あくたで、最初に蚘茉したガむドの範囲で詊すこずにしたす。

QuarkusのOpenID Connect Adapterは、以䞋のモゞュヌルがベヌスになっおいるようです。

The OAuth2 auth provider - Vert.x

たた、QuarkusのSecurity Extensionにも統合されたす。

https://github.com/quarkusio/quarkus/tree/0.27.0/extensions/security

たた、CORSに぀いおは今回は扱いたせんが、ガむドはこちら。

CORS filter

ずたあ、前眮きはこれくらいにしお詊しおみたしょう。

環境

今回の環境は、こちらです。

$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (build 1.8.0_222-8u222-b10-1ubuntu1~18.04.1-b10)
OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)


$ mvn -version
Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-28T00:06:16+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_222, vendor: Private Build, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-66-generic", arch: "amd64", family: "unix"


$ $GRAALVM_HOME/bin/native-image --version
GraalVM Version 19.2.1 CE

Keycloakは、7.0.1を䜿いたす。

2019-11-02 10:20:29,839 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 7.0.1 (WildFly Core 9.0.2.Final) started in 20519ms - Started 589 of 884 services (601 services are lazy, passive or on-demand)

Keycloakは、172.17.0.2のIPアドレスで動䜜しおいるものずしたす。

Keycloakの管理ナヌザヌも䜜成。

$ bin/add-user-keycloak.sh -u keycloak-admin -p password
$ bin/jboss-cli.sh -c --command=reload

お題

Quarkus䞊で、REST APIを䜜成したすが、䞀郚のAPIはアクセス可胜なロヌルを絞ったり、認蚌必須にしたりしたす。

たた、APIによっおは、認蚌されおいるナヌザヌの情報JWTクレヌム・セットを返すようにしたりしたしょう。

Keycloak䞊には、以䞋の情報でRealmやナヌザヌを䜜成したす。

  • Realm 
 demo-api
  • Client-Id 
 sample-rest-api
  • Role 
 users、adminsの2぀
  • User 
 user001users Role、admin001admins Roleの2぀

これらの前提で、進めおいきたす。

サンプルアプリケヌションの䜜成

たずは、プロゞェクトを䜜成したす。Extensionに「oidc」を加えおおくこずがポむントです。

$ mvn io.quarkus:quarkus-maven-plugin:0.27.0:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=resteasy-oidc \
    -Dextensions="oidc, resteasy-jackson"

$ cd resteasy-oidc

「resteasy-jackson」は、JSONを扱うためですが、JSON-BではなくJacksonにしたのはなんずなくです。

たずは、未認蚌状態でアクセスできるJAX-RSリ゜ヌスクラスを䜜成。
src/main/java/org/littlewings/quarkus/oidc/AnonymousResource.java

package org.littlewings.quarkus.oidc;

import java.security.Principal;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.security.identity.SecurityIdentity;

@Path("anonymous")
public class AnonymousResource {
    @Inject
    SecurityIdentity securityIdentity;

    @GET
    @Path("greeting")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello World!!";
    }

    @GET
    @Path("user-info")
    @Produces(MediaType.APPLICATION_JSON)
    public Principal userInfo() {
        System.out.println(securityIdentity.getPrincipal().getClass().getName());
        return securityIdentity.getPrincipal();
    }
}

ずりあえずメッセヌゞを返すだけのリ゜ヌスメ゜ッドず、認蚌されおいればナヌザヌの情報を返すメ゜ッドを甚意しおみたした。

ナヌザヌの情報にアクセスするためには、SecurityIdentityを䜿うようです。

    @Inject
    SecurityIdentity securityIdentity;


    @GET
    @Path("user-info")
    @Produces(MediaType.APPLICATION_JSON)
    public Principal userInfo() {
        System.out.println(securityIdentity.getPrincipal().getClass().getName());
        return securityIdentity.getPrincipal();
    }

SecurityIdentity#getPrincipalで、珟圚のナヌザヌ情報にアクセスするこずができたす。

他にも、珟圚が匿名ナヌザヌであるかどうかだったり、ロヌルなどの確認もできたす。

SecurityIdentityがどのようなむンタヌフェヌスを持぀かは、こちらで確認するずよいでしょう。

https://github.com/quarkusio/quarkus-security/blob/1.0.0.Beta1/src/main/java/io/quarkus/security/identity/SecurityIdentity.java

QuarkusのSecurity Extensionに、このむンタヌフェヌスの実装がありたす。

https://github.com/quarkusio/quarkus/tree/0.27.0/extensions/security/runtime/src/main/java/io/quarkus/security/runtime

次。

usersたたはadminsロヌルを持っおいれば、アクセス可胜なJAX-RSリ゜ヌスクラス。
src/main/java/org/littlewings/quarkus/oidc/UsersResource.java

package org.littlewings.quarkus.oidc;

import java.security.Principal;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.security.identity.SecurityIdentity;

@Path("users")
public class UsersResource {
    @Inject
    SecurityIdentity securityIdentity;

    @GET
    @Path("user-info")
    @RolesAllowed({"users", "admins"})
    @Produces(MediaType.APPLICATION_JSON)
    public Principal userInfo() {
        return securityIdentity.getPrincipal();
    }
}

今回は、users、admins䞡方アクセス可胜にしおいたす。

    @RolesAllowed({"users", "admins"})

@RolesAllowedアノテヌションは、Java EEで定矩されおいるものであり、クラスたたはメ゜ッドに付䞎するこずができたす。

RolesAllowed (Java(TM) EE 7 Specification APIs)

次。

adminsロヌルのみ、蚱可するリ゜ヌス。
src/main/java/org/littlewings/quarkus/oidc/AdminsResource.java

package org.littlewings.quarkus.oidc;

import java.security.Principal;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.security.identity.SecurityIdentity;

@Path("admins")
public class AdminsResource {
    @Inject
    SecurityIdentity securityIdentity;

    @GET
    @Path("user-info")
    @RolesAllowed("admins")
    @Produces(MediaType.APPLICATION_JSON)
    public Principal userInfo() {
        return securityIdentity.getPrincipal();
    }
}

最埌に、認蚌されおいる時のみアクセス可胜なJAX-RSリ゜ヌスクラス。
src/main/java/org/littlewings/quarkus/oidc/AuthenticatedResource.java

package org.littlewings.quarkus.oidc;

import java.security.Principal;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

@Path("authenticated")
public class AuthenticatedResource {
    @Inject
    SecurityIdentity securityIdentity;

    @GET
    @Path("user-info")
    @Authenticated
    @Produces(MediaType.APPLICATION_JSON)
    public Principal guardedUserInfo() {
        return securityIdentity.getPrincipal();
    }
}

この堎合は、@Authenticatedアノテヌションを䜿甚したす。@AuthenticatedアノテヌションはQuarkusが提䟛するもので、こちらも
クラスおよびメ゜ッドに付䞎するこずができたす。

https://github.com/quarkusio/quarkus-security/blob/1.0.0.Beta1/src/main/java/io/quarkus/security/Authenticated.java

蚭定は、最䜎限の項目にしたした。
src/main/resources/application.properties

quarkus.oidc.auth-server-url=http://172.17.0.2:8080/auth/realms/demo-api
quarkus.oidc.client-id=sample-rest-api

OpenID Connectの認蚌ProviderぞのアクセスURLず、Client IDがあれば利甚するこずができたす。

蚭定項目は、こちら。

Configuring using the application.properties file

ずいうか、この䜿い方だずClient Secretすら芁りたせん 。

これら2぀を蚭定しなかった堎合は、アプリケヌションがうたく動䜜しなくなりたす。

auth-server-urlを未定矩にした堎合。

java.lang.RuntimeException: java.net.MalformedURLException: no protocol: /.well-known/openid-configuration
    at io.vertx.ext.auth.oauth2.impl.OAuth2API.makeRequest(OAuth2API.java:112)
    at io.vertx.ext.auth.oauth2.providers.OpenIDConnectAuth.discover(OpenIDConnectAuth.java:42)
    at io.vertx.ext.auth.oauth2.providers.KeycloakAuth.discover(KeycloakAuth.java:120)
    at io.quarkus.oidc.runtime.OidcRecorder.setup(OidcRecorder.java:49)
    at io.quarkus.deployment.steps.OidcBuildStep$setup24.deploy_0(OidcBuildStep$setup24.zig:92)
    at io.quarkus.deployment.steps.OidcBuildStep$setup24.deploy(OidcBuildStep$setup24.zig:36)
    at io.quarkus.runner.ApplicationImpl1.doStart(ApplicationImpl1.zig:137)
    at io.quarkus.runtime.Application.start(Application.java:94)
    at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:143)
    at io.quarkus.dev.DevModeMain.doStart(DevModeMain.java:176)
    at io.quarkus.dev.DevModeMain.start(DevModeMain.java:94)
    at io.quarkus.dev.DevModeMain.main(DevModeMain.java:66)
Caused by: java.net.MalformedURLException: no protocol: /.well-known/openid-configuration
    at java.net.URL.<init>(URL.java:600)
    at java.net.URL.<init>(URL.java:497)
    at java.net.URL.<init>(URL.java:446)
    at io.vertx.ext.auth.oauth2.impl.OAuth2API.makeRequest(OAuth2API.java:93)
    ... 11 more

client-idを未定矩にした堎合。

Caused by: java.lang.IllegalArgumentException: Configuration missing. You need to specify [clientId]
    at io.vertx.ext.auth.oauth2.impl.flow.AbstractOAuth2Flow.throwIfNull(AbstractOAuth2Flow.java:48)
    at io.vertx.ext.auth.oauth2.impl.flow.AuthCodeImpl.<init>(AuthCodeImpl.java:39)
    at io.vertx.ext.auth.oauth2.impl.OAuth2AuthProviderImpl.<init>(OAuth2AuthProviderImpl.java:68)
    at io.vertx.ext.auth.oauth2.OAuth2Auth.create(OAuth2Auth.java:132)
    at io.vertx.ext.auth.oauth2.providers.OpenIDConnectAuth.lambda$discover$1(OpenIDConnectAuth.java:79)
    at io.vertx.ext.auth.oauth2.impl.OAuth2API.lambda$null$1(OAuth2API.java:129)
    at io.vertx.core.http.impl.HttpClientResponseImpl$BodyHandler.notifyHandler(HttpClientResponseImpl.java:292)
    at io.vertx.core.http.impl.HttpClientResponseImpl.lambda$bodyHandler$0(HttpClientResponseImpl.java:193)
    at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:248)
    ... 35 more

確認する

では、動かしお確認しおみたしょう。

パッケヌゞングしお起動。

$ mvn package
$ java -jar target/resteasy-oidc-1.0-SNAPSHOT-runner.jar

起動するず、Security ExtensionやVert.x Extensionも有効になっおいるこずがわかりたす。

2019-11-02 20:35:41,365 INFO  [io.quarkus] (main) Installed features: [cdi, oidc, resteasy, resteasy-jackson, security, vertx]

では、ドキュメントに沿っお詊しおみたしょう。

Testing the Application

たずは、未認蚌状態でアクセス。

$ curl localhost:8080/anonymous/greeting
Hello World!!

ナヌザヌ情報を取埗しようずしおみたす。

$ curl localhost:8080/anonymous/user-info
{"name":""}

無名のナヌザヌみたいです。

この時の、Principalは以䞋のようです。

io.quarkus.security.runtime.AnonymousIdentityProvider$1

゜ヌスコヌドは、こちらですね。

https://github.com/quarkusio/quarkus/blob/0.27.0/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/AnonymousIdentityProvider.java#L19

SecurityIdentityも、それっぜいのがありたす。

https://github.com/quarkusio/quarkus/blob/0.27.0/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/AnonymousIdentityProvider.java#L26

ロヌルでアクセス暩限を制限したり、認蚌必須にしおいるリ゜ヌスメ゜ッドに察しおは、すべおHTTPステヌタスコヌド401が返っおきたす。

$ curl -i localhost:8080/users/user-info
HTTP/1.1 401 Unauthorized
Content-Length: 0


$ curl -i localhost:8080/admins/user-info
HTTP/1.1 401 Unauthorized
Content-Length: 0


$ curl -i localhost:8080/authenticated/user-info
HTTP/1.1 401 Unauthorized
Content-Length: 0

ちなみに、認蚌サヌバヌぞリダむレクト、ずいうこずにはならないようです。
※これをやりたかったら、続きのガむド みたいなのですが 

では、Keycloakから、アクセストヌクンを取埗しおみたす。

たずは、user001から。

$ access_token=$( \
curl -X POST http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/token \
    --user sample-rest-api:client-secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=user001&password=password&grant_type=password' | jq --raw-output '.access_token' \
)

取埗したアクセストヌクンを䜿っお、ナヌザヌ情報を取埗しおみたす。最初に、認蚌やロヌルの制限をかけおいないリ゜ヌスメ゜ッドから。

$ curl localhost:8080/anonymous/user-info -H "Authorization: Bearer "$access_token
{"rawToken":null,"claims":{"claimsMap":{"jti":"3026f20d-76be-4676-a5a9-d6e3535da1c8","exp":1572699465,"nbf":0,"iat":1572699165,"iss":"http://172.17.0.2:8080/auth/realms/demo-api","aud":"account","sub":"f4a47f71-c5c3-4364-920e-58b284df1523","typ":"Bearer","azp":"sample-rest-api","auth_time":0,"session_state":"78ebf1fc-247a-4f80-a56f-1c53b681fc2f","acr":"1","allowed-origins":[{"string":"http://localhost:8080","valueType":"STRING","chars":"http://localhost:8080"}],"realm_access":{"roles":[{"string":"offline_access","valueType":"STRING","chars":"offline_access"},{"string":"uma_authorization","valueType":"STRING","chars":"uma_authorization"},{"string":"users","valueType":"STRING","chars":"users"}]},"resource_access":{"account":{"roles":[{"string":"manage-account","valueType":"STRING","chars":"manage-account"},{"string":"manage-account-links","valueType":"STRING","chars":"manage-account-links"},{"string":"view-profile","valueType":"STRING","chars":"view-profile"}]}},"scope":"profile email","email_verified":false,"preferred_username":"user001"},"rawJson":"{\"jti\":\"3026f20d-76be-4676-a5a9-d6e3535da1c8\",\"exp\":1572699465,\"nbf\":0,\"iat\":1572699165,\"iss\":\"http://172.17.0.2:8080/auth/realms/demo-api\",\"aud\":\"account\",\"sub\":\"f4a47f71-c5c3-4364-920e-58b284df1523\",\"typ\":\"Bearer\",\"azp\":\"sample-rest-api\",\"auth_time\":0,\"session_state\":\"78ebf1fc-247a-4f80-a56f-1c53b681fc2f\",\"acr\":\"1\",\"allowed-origins\":[\"http://localhost:8080\"],\"realm_access\":{\"roles\":[\"offline_access\",\"uma_authorization\",\"users\"]},\"resource_access\":{\"account\":{\"roles\":[\"manage-account\",\"manage-account-links\",\"view-profile\"]}},\"scope\":\"profile email\",\"email_verified\":false,\"preferred_username\":\"user001\"}","notBefore":{"value":0,"valueInMillis":0},"issuedAt":{"value":1572699165,"valueInMillis":1572699165000},"jwtId":"3026f20d-76be-4676-a5a9-d6e3535da1c8","expirationTime":{"value":1572699465,"valueInMillis":1572699465000},"issuer":"http://172.17.0.2:8080/auth/realms/demo-api","audience":["account"],"subject":"f4a47f71-c5c3-4364-920e-58b284df1523","claimNames":["jti","exp","nbf","iat","iss","aud","sub","typ","azp","auth_time","session_state","acr","allowed-origins","realm_access","resource_access","scope","email_verified","preferred_username"]},"audience":["account"],"groups":[],"claimNames":["sub","resource_access","email_verified","allowed-origins","raw_token","iss","typ","preferred_username","aud","acr","nbf","realm_access","azp","auth_time","scope","exp","session_state","iat","jti"],"name":"user001","expirationTime":1572699465,"issuer":"http://172.17.0.2:8080/auth/realms/demo-api","subject":"f4a47f71-c5c3-4364-920e-58b284df1523","tokenID":"3026f20d-76be-4676-a5a9-d6e3535da1c8","issuedAtTime":1572699165}

長いので、ナヌザヌ名を絞っお取埗しおみたしょう。

$ curl -s localhost:8080/anonymous/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
user001

OKそうです。

なお、この時のPrincipalは、以䞋のようになりたす。

io.quarkus.oidc.runtime.OidcJwtCallerPrincipal

こちらですね。

https://github.com/quarkusio/quarkus/blob/0.27.0/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJwtCallerPrincipal.java

usersおよびadminsロヌルでアクセス制限をかけおいるリ゜ヌスメ゜ッドおよび、認蚌のみ制限をかけたリ゜ヌスメ゜ッドには
アクセスするこずができたす。

$ curl -s localhost:8080/users/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
user001


$ curl -s localhost:8080/authenticated/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
user001

adminsロヌルのみアクセス可胜なリ゜ヌスメ゜ッドに察しおは、HTTPステヌタスコヌド403ずなりたす。

$ curl -i localhost:8080/admins/user-info -H "Authorization: Bearer "$access_token
HTTP/1.1 403 Forbidden
content-length: 0

では、今床はadmin001ナヌザヌに切り替えたす。

$ access_token=$( \
curl -X POST http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/token \
    --user sample-rest-api:client-secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=admin001&password=password&grant_type=password' | jq --raw-output '.access_token' \
)

この堎合、いずれのリ゜ヌスメ゜ッドにもアクセスできるようになりたす。

$ curl -s localhost:8080/anonymous/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
admin001


$ curl -s localhost:8080/users/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
admin001


$ curl -s localhost:8080/admins/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
admin001


$ curl -s localhost:8080/authenticated/user-info -H "Authorization: Bearer "$access_token | jq --raw-output '.name'
admin001

ネむティブむメヌゞで実行する堎合は、こちら。

$ mvn -P native package
$ ./target/resteasy-oidc-1.0-SNAPSHOT-runner

結果は、倉わらず ず蚀いたいずころですが、先ほどのコヌドだずPrincipalのJSONシリアラむズに倱敗するので、各リ゜ヌスクラスに
メ゜ッドを远加したした 。

Principal#getNameの結果だけを返すメ゜ッドで。

@Path("anonymous")
public class AnonymousResource {

    /* 省略 */

    @GET
    @Path("name")
    @Produces(MediaType.TEXT_PLAIN)
    public String name() {
        return securityIdentity.getPrincipal().getName();
    }
}


@Path("users")
public class UsersResource {

    /* 省略 */

    @GET
    @Path("name")
    @RolesAllowed({"users", "admins"})
    @Produces(MediaType.TEXT_PLAIN)
    public String name() {
        return securityIdentity.getPrincipal().getName();
    }
}


@Path("admins")
public class AdminsResource {

    /* 省略 */

    @GET
    @Path("name")
    @RolesAllowed("admins")
    @Produces(MediaType.TEXT_PLAIN)
    public String name() {
        return securityIdentity.getPrincipal().getName();
    }
}


@Path("authenticated")
public class AuthenticatedResource {

    /* 省略 */

    @GET
    @Path("name")
    @Authenticated
    @Produces(MediaType.TEXT_PLAIN)
    public String name() {
        return securityIdentity.getPrincipal().getName();
    }
}

こちらを䜿っお、確認するようにしたした。

未認蚌状態。

$ curl -i localhost:8080/anonymous/name
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain;charset=UTF-8


$ curl -i localhost:8080/users/name
HTTP/1.1 401 Unauthorized
Content-Length: 0


$ curl -i localhost:8080/admins/name
HTTP/1.1 401 Unauthorized
Content-Length: 0


$ curl -i localhost:8080/authenticated/name
HTTP/1.1 401 Unauthorized
Content-Length: 0

user001でアクセス。

access_token=$( \
curl -X POST http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/token \
    --user sample-rest-api:client-secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=user001&password=password&grant_type=password' | jq --raw-output '.access_token' \
)


$ curl localhost:8080/anonymous/name -H "Authorization: Bearer "$access_token
user001


$ curl localhost:8080/users/name -H "Authorization: Bearer "$access_token
user001


$ curl localhost:8080/authenticated/name -H "Authorization: Bearer "$access_token
user001


$ curl -i localhost:8080/admins/name -H "Authorization: Bearer "$access_token
HTTP/1.1 403 Forbidden
Content-Length: 9
Content-Type: text/plain;charset=UTF-8

Forbidden

admin001でアクセス。

access_token=$( \
curl -X POST http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/token \
    --user sample-rest-api:client-secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=admin001&password=password&grant_type=password' | jq --raw-output '.access_token' \
)


$ curl localhost:8080/anonymous/name -H "Authorization: Bearer "$access_token
admin001


$ curl localhost:8080/users/name -H "Authorization: Bearer "$access_token
admin001


$ curl localhost:8080/admins/name -H "Authorization: Bearer "$access_token
admin001


$ curl localhost:8080/authenticated/name -H "Authorization: Bearer "$access_token
admin001

ちょっず゜ヌスコヌドを倉えおしたいたしたが、認蚌の有無やロヌルによる動䜜が同じこずは確認できたした、ず。

もう少し、コヌドを远う

OpenID Connect Extensionを远加したQuarkusは、起動時に認蚌サヌバヌを探しにいきたす。

このあたりですね。凊理に䜿われおいるクラス名だけ芋るず、Keycloak 

https://github.com/quarkusio/quarkus/blob/0.27.0/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java#L49

たた、@RolesAllowedや@Authenticatedアノテヌションを付䞎するず、むンタヌセプタヌが起動したす。

ここにありたすね。

https://github.com/quarkusio/quarkus/tree/0.27.0/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor

他のアノテヌションずしおは、@DenyAll、@PermitAllがあるようです。

たた、むンタヌセプタヌ内から呌び出され、トヌクンの情報に察しお、認蚌・認可に関する確認を行っおいるのはこちらのクラスです。

https://github.com/quarkusio/quarkus/blob/0.27.0/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/SecurityConstrainer.java

自分で䜜成するプログラム内で、こういったこずをやりたい堎合はSecurityIdentityを䜿っおif文などを曞いたりするこずになるのでしょう。

https://github.com/quarkusio/quarkus-security/blob/1.0.0.Beta1/src/main/java/io/quarkus/security/identity/SecurityIdentity.java

たずめ

QuarkusのOpenID Connect Extensionを、リ゜ヌス保護のみですが、詊しおみたした。

未認蚌状態でアクセスしたら、Keycloakのログむンペヌゞにリダむレクトされたりしお欲しいのですが、そのあたりはたた今床
確認しおみようず思いたす。

このあたりを芋るこずになるず思いたす。

Quarkus - Protecting Web Applications Using OpenID Connect

が、今は少し早そうです。

unittestラむブラリで、Pythonのテストコヌドを曞いお実行する

これは、なにをしたくお曞いたもの

Pythonを勉匷するにあたっお、テストコヌドたわりに぀いお少し抌さえおおいた方がいいかなぁず思いたしお。

Pythonには、いく぀かテストをサポヌトするラむブラリ、ツヌルがあるようです。

コードのテスト — The Hitchhiker's Guide to Python

Testing Your Code — The Hitchhiker's Guide to Python

今回は基本である、unittestを䜿っおみるこずにしたした。

unittest

Python暙準に含たれる、JUnitに觊発されたテスティングフレヌムワヌクです。

26.4. unittest --- ユニットテストフレームワーク — Python 3.6.9 ドキュメント

unittest ナニットテストフレヌムワヌクは元々 JUnit に觊発されたもので、 他の蚀語の䞻芁なナニットテストフレヌムワヌクず同じような感じです。 テストの自動化、テスト甚のセットアップやシャットダりンのコヌドの共有、テストのコレクション化、そしお報告フレヌムワヌクからのテストの独立性をサポヌトしおいたす。

テストケヌスの䜜成のためのクラスやアサヌションを含み、ランナヌも提䟛したす。

アサヌションに぀いおは、こちら。

アサヌトメ゜ッド䞀芧

assertAlmostEqual
※assertAlmostEqualメ゜ッドのリファレンスの䞊に、䞀芧がありたす

実行に぀いおは、unittestをコマンドラむンからモゞュヌル指定で実行するこずで行いたす。

コマンドラむンむンタヌフェむス

特定のディレクトリの配䞋のテストコヌドを怜出する、テストディスカバリも可胜です。

テストディスカバリ

ずころで、コヌドのテストでも玹介されおいたすが、pytestずいうものも抌さえおおいた方がよさそうなので、こちらもそのうち。

pytest: helps you write better programs — pytest documentation

環境

今回の環境は、こちらです。

$ python3 -V
Python 3.6.8

お題ずプロゞェクト構成

テスト察象のコヌドアプリケヌションコヌドず、テストコヌドを同じディレクトリに配眮しお、サンプル的に動かしおみおも
いいのですが、せっかくなら実際に䜿う時の構成を意識しおみたいなぁず思いたす。

テストコヌドを配眮するディレクトリ構成は、pytestのドキュメントを参考に。

Choosing a test layout / import rules

テストコヌドをアプリケヌションコヌドの倖に眮くスタむルず

Tests outside application code

テストコヌドをアプリケヌションコヌドの䞭に眮くスタむルがあるようです。

Tests as part of application code

今回は、テストコヌドをアプリケヌションコヌドの倖に眮くこずにしたした。

こんなディレクトリ構成にしたす。

sample  ## ← アプリケヌションコヌドを眮く
tests  ## ← テストコヌドを眮く

テストコヌドのディスカバリも詊すために、アプリケヌションコヌドを2぀のファむルで䜜成し、察応するテストも2぀甚意する構成に
したいず思いたす。

テストを䜜成しお、実行しおみる

たず最初に、アプリケヌションコヌドを曞きたす。 sample/calc.py

class Calc:
    def add(self, x, y):
        return x + y

    def minus(self, x, y):
        return x - y

    def multiply(self, x, y):
        return x * y

    def divide(self, x, y):
        return x / y

これに察応する、テストコヌドを曞きたしょう。

unittestを䜿う堎合、テストはTestCaseクラスのサブクラスずしお䜜成するようです。

基本的な䟋

たた、テストを行うメ゜ッド名は、「test」で始たる必芁があるようです。

テストケヌスは、 unittest.TestCase のサブクラスずしお䜜成したす。メ゜ッド名が test で始たる䞉぀のメ゜ッドがテストです。テストランナヌはこの呜名芏玄によっおテストを行うメ゜ッドを怜玢したす。

アサヌションは、unittestずいうかTestCaseクラスが提䟛するメ゜ッドを䜿甚しお行いたす。

で、䜜成したのがこちら。
tests/test_calc.py

import unittest

from sample.calc import Calc

class CalcTestCase(unittest.TestCase):
    def setUp(self):
        print("setUp!!")

    def tearDown(self):
        print("tearDown!!")

    def test_add(self):
        sut = Calc()
        self.assertEqual(sut.add(1, 3), 4)

    def test_minus(self):
        sut = Calc()
        self.assertEqual(sut.minus(5, 3), 2)

    def test_multiply(self):
        sut = Calc()
        self.assertEqual(sut.multiply(2, 3), 6)

    def test_divide(self):
        sut = Calc()
        self.assertEqual(sut.divide(10, 2), 5)

    def foo(self):
        print("foo!!")

しれっず、「test」で始たらないメ゜ッドも含めおありたす。

テストメ゜ッドごずに実行するsetUpやtearDownも曞いおみたした。クラス単䜍のsetUpClass、tearDownClassなどもあるようなので、
ドキュメントを参照するずよいでしょう。

class unittest.TestCase

クラスずモゞュヌルのフィクスチャ

setUpClass ず tearDownClass

テストを実行しおみたす。

$ python3 -m unittest tests.test_calc
setUp!!
tearDown!!
.setUp!!
tearDown!!
.setUp!!
tearDown!!
.setUp!!
tearDown!!
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

setUpやtearDownが、テストメ゜ッドごずに動いおいるような感じがしたす。

が、詳现がわからないので「-v」を付けおみたす。

$ python3 -m unittest tests.test_calc -v
test_add (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_divide (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_minus (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_multiply (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

なるほど、これだず実行されたテストメ゜ッドもわかりたすね。

以䞋のメ゜ッドが察象に含たれおいないこずも確認できたした。

    def foo(self):
        print("foo!!")

たた、テストに倱敗するようなコヌドになっおいる堎合は、こんな衚瀺になりたす。

======================================================================
FAIL: test_add (tests.test_calc.CalcTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/tests/test_calc.py", line 14, in test_add
    self.assertEqual(sut.add(1, 3), 5)
AssertionError: 4 != 5

あずもうひず぀、アプリケヌションコヌドを远加しお
sample/message.py

class Decorator:
    def decorate(self, message, character):
        return "{0}{1}{2}".format(character, message, character)

テストも足しおおきたしょう。
tests/test_message.py

import unittest

from sample.message import Decorator

class DecoratorTestCase(unittest.TestCase):
    def test_decorate(self):
        sut = Decorator()
        self.assertEqual(sut.decorate("Hello World!!", "***"), "***Hello World!!***")

確認。

$ python3 -m unittest tests.test_message -v
test_decorate (tests.test_message.DecoratorTestCase) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

なお、テスト察象を耇数指定しお実行するこずもできたす。

$ python3 -m unittest tests.test_calc tests.test_message -v
test_add (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_divide (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_minus (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_multiply (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_decorate (tests.test_message.DecoratorTestCase) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK
unittest#main

ドキュメントの以䞋の郚分にも曞いおいるのですが、

基本的な䟋

基本的な䜿い方だず、unittest#mainを呌び出すように、テストコヌドに曞くようです。

if __name__ == '__main__':
    unittest.main()

で、Pythonコマンドで盎接実行する、ず。

$ python3 test_example.py

これでも良いのですが、今回のようにアプリケヌションコヌドずテストコヌドを別々にする方法だず、モゞュヌルのパス解決で
困ったこずになったので、今回はパス 。

テストディスカバリを行う

ここたでは、テストコヌドをひず぀ひず぀指定しお実行しおきたしたが、テストディスカバリを䜿うずテストコヌドを芋぀けお
くれるようです。

テストディスカバリ

シンプルな実行方法は、以䞋だずか。

$ python3 -m unittest

### たたは
$ python3 -m unittest discover

詊しおみたす。

$ python3 -m unittest

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

 テストが実行されなかったようです。

こここで、TestLoader#discoverの説明を読んでみたす。

TestLoader / discover

指定された開始ディレクトリからサブディレクトリに再垰するこずですべおのテストモゞュヌルを怜玢し、それらを含む TestSuite オブゞェクトを返したす。pattern にマッチしたテストファむルだけがロヌドの察象になりたす。

モゞュヌルに぀いお、もうちょっず調べおみたす。

パッケヌゞ

あるディレクトリを、パッケヌゞが入ったディレクトリずしおPython に扱わせるには、ファむル __init__.py が必芁です。

どうやら、__init__.pyが必芁な雰囲気がありたす。

䜜成。

$ touch tests/__init__.py

再床、実行。

$ python3 -m unittest
setUp!!
tearDown!!
.setUp!!
tearDown!!
.setUp!!
tearDown!!
.setUp!!
tearDown!!
..
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

今床は動きたしたね。

「-v」オプションを指定しおみたしょう。

$ python3 -m unittest -v
test_add (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_divide (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_minus (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_multiply (tests.test_calc.CalcTestCase) ... setUp!!
tearDown!!
ok
test_decorate (tests.test_message.DecoratorTestCase) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

たた、ディスカバリを開始するディレクトリを「-s」オプションで指定したり、テストが曞かれたファむル名のパタヌンを「-p」で
指定したりできたすが、これらを䜿う堎合は「discover」サブコマンドの指定が必須になりたす。

「discover」サブコマンドの指定なしで、「-p」オプションを指定するず゚ラヌになりたすが

$ python3 -m unittest -p 'test_*.py'
usage: python3 -m unittest [-h] [-v] [-q] [--locals] [-f] [-c] [-b]
                           [tests [tests ...]]
python3 -m unittest: error: unrecognized arguments: -p

「discover」サブコマンドを指定するず、動䜜したす。

$ python3 -m unittest discover -p 'test_*.py'
setUp!!
tearDown!!
.setUp!!
tearDown!!
.setUp!!
tearDown!!
.setUp!!
tearDown!!
..
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

たずめ

Pythonの暙準テストラむブラリであるunittestを、実行方法の点からちょっず芋おみたした。

アサヌションや、その他の機胜に぀いおはあたり芋れおいたせんが、ずりあえず初歩的な䜿い方ずしおはわかった感じかなず。