18
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

KeycloakのRoleを使ってHasuraのRBACを行ってみた。

Last updated at Posted at 2021-12-16

はじめに

GraphQLのバックエンドサービスであるHasuraはDBへのアクセス制御をRoleによって行っています。本記事ではKeycloakのRoleを使ってHasuraのRole Based Access Control(RBAC)を試してみたので、その内容を紹介します。

本記事は、あくまで執筆者の見解であり、日立製作所及びHasuraの公式なドキュメントではありません。

Hasuraの概要

HasuraはDBにアクセスするためのGraphQL APIを生成するバックエンドサービスです。HasuraはクライアントとDBの間に入り,DBスキーマからGraphQL APIを構築したり、DBにアクセスするGraphQL APIをSQLに自動変換してくれます。これによりGraphQL APIの開発を迅速に進めることが可能になります。HasuraではDBとしては特に相性の良いPostgreSQLの他、Microsoft SQL Server、Amazon Aurora、Google BigQueryを用いることができます。その他、ORACLE、mongoDB、MySQL、Elasticsearchなどをサポート予定です。

Hasuraでの認証・認可

Hasuraにおいてアクセス制御(認可)はRoleを用いて行います。Role,Table, CRUD毎にPermissionと呼ばれるアクセスルールを設定し、DBの行単位、列単位での制御ができます。またHausraでは権限のない行や列はアクセスできなくなるだけでなく、スキーマからも消失するという特徴があります。
一方でユーザ認証はHasura外で行う必要があり、認証方式としてwebhook認証とJWT認証の2種類が提供されています。いずれもSession Variablesと呼ばれる変数(X-Hasura-RoleやX-Hasura-User-Id等)に値をセットし、Hausraに渡すことでユーザの識別やアクセス制御を行います。webhook認証ではHasuraがwebhookにHTTPリクエストを送信しwebhook側でTokenの検証が行われた後、レスポンスとしてRoleやSession Variablesが返却されます。
JWT認証は認証やアクセス制御等の属性情報(claim)をJSON形式で記述したJSON Web Token(JWT)を用いる認証方式です。本手法では認可サーバーでJWTを生成する際にRoleやSession Variablesの情報をcustom claimとして含めます。クライアント側はこのJWTをリクエストヘッダに含めてHasuraにHTTPリクエストを送信し、HasuraでJWTの検証を行います。

検証内容概要

今回は認可サーバーとしてKeycloakを使用し、HasuraのJWT認証を試します。
HasuraがアクセスするDBにはPostgreSQLを使用し、以下の表のようなテーブルUser_Infoを作成します。

user_num name department password
1 太郎 財務部 aa1
2 次郎 法務部 bki4
3 三郎 総務部 csu27
4 四郎 法務部 dte256
5 五郎 財務部 eno3125
6 六郎 財務部 fha46656

次にHasuraで3つのRole"manager"と"user","finance"を作成し、各Roleでテーブルにアクセスしたときの振る舞いを確認します。それぞれのRoleは以下のような権限を有します。

Role名 説明
manager 全てのユーザ情報を取得できる。
user 自らのユーザ情報だけ取得できる。
finance 財務部に所属するユーザの情報の内password以外のデータを取得できる。

環境の構築

今回の検証はdocker-composeを用いて行います。docker-compose.ymlは以下の通りです。注意点としてJWTでの認証を有効にするために"HASURA_GRAPHQL_JWT_SECRET"でJWTを検証するjwks_uriを指定し、"HASURA_GRAPHQL_ADMIN_SECRET"でAdmin権限でアクセスする際のパスワードを設定してください。

sample_environment.png
docker-compose.yml
version: '3.3'
services:

  # Hasura
  hasura:
    image: hasura/graphql-engine:v2.0.10
    ports:
    - "8080:8080"
    depends_on:
    - "pg_hasura"
    - "keycloak"
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@pg_hasura:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
HASURA_GRAPHQL_JWT_SECRET: '{ "type": "RS256", "jwk_url": "http://keycloak:8080/auth/realms/master/protocol/openid-connect/certs" }'
      HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey

  # PostgreSQL Server for Hasura
  pg_hasura:
    image: postgres:12
    restart: always
    volumes:
    - pg_hasura_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgrespassword

  #Keycloak
  keycloak:
    image: jboss/keycloak:15.0.1
    ports:
    - "8081:8080"
    depends_on:
    - "pg_keycloak"
    restart: always
    environment:
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: admin
      DB_VENDOR: postgres
      DB_ADDR: pg_keycloak
      DB_USER: keycloak
      DB_PASSWORD: password

  # PostgreSQL Server for Keycloak
  pg_keycloak:
    image: postgres:12
    restart: always
    volumes:
    - pg_keycloak_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password

volumes:
  pg_hasura_data:
  pg_keycloak_data:

Keycloak側で行う設定

Roleの設定

まずhttps://github.com/janhapke/hasura-keycloakを参考にしてlocalhost:8081/auth/adminにアクセスします。ログインに必要なユーザ名とパスワード名はdocker-compose.yml内のKEYCLOAK_USERとKEYCLOAK_PASSWORDに設定した値になります。ログイン後"Client"として新たにhasuraを作成し、hasuraの"Roles"で上記の"manager","finace","user"を新たに生成します。

Mapperの設定

続いて"Mappers"で3つのMapper"x-hasura-default-role"と"x-hasura-allowed-roles","x-hasura-user-num"を作成します。各Mapperの説明は以下の通りです。

Mapper名 説明
x-hasura-allowed-roles 認証したユーザに割り当てるRole一覧をセットします。この中で実際に割り当てたいRoleをX-Hasura-Roleヘッダで指定します。必ずclaimに含めてください。
x-hasura-default-role X-Hasura-RoleヘッダでRoleを指定しない場合に割り当てるRoleを設定します。必ずclaimに含めてください。
x-hasura-user-num Roleとは関係ないですがHasuraでSession Variablesとして使用します。claimに必ずしも含める必要はありません。

各Mapperは以下のように設定します。注意点としては公式のドキュメントhttps://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html#にも述べられているようにMapperのTypeは必ずStringにして下さい。

項目名 設定値
Name x-hasura-default-role
Mapper Type Hardcoded claim
Token Claim Name https://hasura\.io/jwt/claims.x-hasura-default-role
Claim value user
Claim JSON Type String
項目名 設定値
Name x-hasura-allowed-roles
Mapper Type User Client Role
Client Id hasura
Multivalued ON
Token Claim Name https://hasura\.io/jwt/claims.x-hasura- allowed-roles
Claim JSON Type String
項目名 設定値
Name x-hasura-user-num
Mapper Type User Attribute
User Attribute num
Token Claim Name https://hasura\.io/jwt/claims.x-hasura-user-num
Claim JSON Type String

Userの設定

最後に"Users"でアクセストークンを取得するユーザを編集します(ここではadmin)。そして"Attributes"でKeyにnum、Valueに3を追加します。続いて"Role Mappings"で"Client Roles"にhasuraを選択し、"Available Roles"にある"manager"と"user","finance"を"Assigned Roles"に追加してください。

Hasura側で行う設定

HasuraではRoleに対してCRUDそれぞれにPermissionを作成できるのですが、今回は簡略化してRead(Select)操作に関するPermissionのみ設定します。まず"manager"Roleは全てのユーザ情報にアクセスできるようにしたいので、下図のように何も制限しません。

manager.PNG

次に"user"Roleは自らのユーザ情報に一致するデータのみアクセスしたいので、Row select permissionsでKeycloakから取得したJWTに含まれるX-Hasura-User-Numがuser_numと一致する行にのみアクセスできるようにします。そしてColumn select permissionsではすべて選択します。

user.PNG

最後に"finance"Roleは財務部に所属するユーザの情報の内passwordだけ取得できないので、下図のようにRow select permissionsではdepartmentが財務部に一致する行にのみアクセスするように、Column select permissionsではpassword以外の列を選択します。

finance.PNG

検証結果

まずは以下のコマンドを実行し、Keycloakからアクセストークンを取得します。取得したトークンはHasuraへのHTTPリクエストのAuthorizationヘッダに"Bearer 取得したアクセストークン"という形式で入れます。

curl -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=admin" \
  -d "password=admin" \
  -d "grant_type=password" \
  -d "client_id=hasura" \
  http://localhost:8081/auth/realms/master/protocol/openid-connect/token

それでは各Roleでのレスポンスを見てきます。まず"manager"Roleでのレスポンスを確認するためにX-Hasura-Roleヘッダにmanagerをセットし、全フィールドのデータを取得します。

curl -X POST -H "Content-Type: application/json" \
  -H "X-Hasura-Role: manager" \
  -H "Authorization: Bearer 取得したアクセストークン" \
  -d "{ \"query\": \"query { User_Info { user_num name department password }}\"}" \
  http://localhost:8080/v1/graphql | jq

レスポンスは以下のようになり、User_Infoテーブルの全てのデータが返ってきたことがわかります。

{
  "data": {
    "User_Info": [
      {
        "user_num": "1",
        "name": "太郎",
        "department": "財務部",
        "password": "aa1"
      },
      {
        "user_num": "2",
        "name": "次郎",
        "department": "法務部",
        "password": "bki4"
      },
      {
        "user_num": "3",
        "name": "三郎",
        "department": "総務部",
        "password": "csu27"
      },
      {
        "user_num": "4",
        "name": "四郎",
        "department": "法務部",
        "password": "dte256"
      },
      {
        "user_num": "5",
        "name": "五郎",
        "department": "財務部",
        "password": "eno3125"
      },
      {
        "user_num": "6",
        "name": "六郎",
        "department": "財務部",
        "password": "fha46656"
      }
    ]
  }
}

次にx-hasura-allowed-rolesで指定したRoleしか割り当てられないことを確認するために、X-Hasura-RoleヘッダにtestをセットしてHTTPリクエストしてみます。その結果以下のようなエラーがレスポンスされ、x-hasura-allowed-rolesにセットしていないRoleは割り当てられないことが確認できます。

{
  "errors": [
    {
      "extensions": {
        "path": "$",
        "code": "access-denied"
      },
      "message": "Your requested role is not in allowed roles"
    }
  ]
}

またアクセストークンがない場合にもリクエストが失敗することを確認するために、Authorizationヘッダを含めずにHTTPリクエストしてみます。すると以下のようなエラーレスポンスが返却され、リクエストに失敗が確認できます。

{
  "errors": [
    {
      "extensions": {
        "path": "$",
        "code": "invalid-headers"
      },
      "message": "Missing Authorization header in JWT authentication mode"
    }
  ]
}

次にX-Hasura-RoleヘッダにuserをセットしてHTTPリクエストを送ります。Keycloakで認証するユーザ(ここではadmin)のnumを3に設定したので、レスポンスでは以下のようにuser_numが3のユーザ情報のみが返ってきていることが確認できます。また今回x-hasura-default-roleに"user"をセットしているので、HTTPリクエストにX-Hasura-Roleヘッダを含めなくても同様の結果が得られることが確認できると思います。

{
  "data": {
    "User_Info": [
      {
        "user_num": "3",
        "name": "三郎",
        "department": "総務部",
        "password": "csu27"
      }
    ]
  }
}

最後にX-Hasura-RoleヘッダにfinanceをセットしてHTTPリクエストを送ると以下のように"password"fieldがUser_Infoテーブルには存在しないというエラーが返ってきます。

{
  "errors": [
    {
      "extensions": {
        "path": "$.selectionSet.User_Info.selectionSet.password",
        "code": "validation-failed"
      },
      "message": "field \"password\" not found in type: 'User_Info'"
    }
  ]
}

そこで取得したいデータを"password"field以外にすると"department"が財務部のユーザ情報のみレスポンスとして返ってくることが確認できます。

curl -X POST -H "Content-Type: application/json" \
  -H "X-Hasura-Role: finance" \
  -H "Authorization: Bearer 取得したアクセストークン" \
  -d "{ \"query\": \"query { User_Info { user_num name department }}\"}" \
  http://localhost:8080/v1/graphql | jq
{
  "data": {
    "User_Info": [
      {
        "user_num": "1",
        "name": "太郎",
        "department": "財務部"
      },
      {
        "user_num": "5",
        "name": "五郎",
        "department": "財務部"
      },
      {
        "user_num": "6",
        "name": "六郎",
        "department": "財務部"
      }
    ]
  }
}

おわりに

本記事では、KeycloakのRoleでHasuraのRBACを行う方法をご紹介しました。X-Hasura-RoleヘッダにセットするRoleに応じて、取得するデータが変化したことが確認できたと思います。本手法はClient側のAppがKeycloakからアクセストークンを取得します。そのため、Appによって取得するRoleを切り替えるといったこともできると思います。

18
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?