サービスアカウントを切り替えて BigQuery のアクセス権限を制御してみた

2020.04.03

こんにちは、みかみです。

やりたいこと

アプリなどから BigQuery にアクセスする場合に、アクセス制御するにはどうすれば良いのか知りたい

サービスアカウントを切り替えるということ

BigQuery のアクセス権は、アカウントに付与されたロールで制御可能です。 なお、BigQuery には SQL の GRANT 構文はなく、現在のところ、テーブル、ビュー、列、行単位でのアクセス制御はできません。

アプリなどからクライアントライブラリを使って BigQuery API 経由で BigQuery にアクセスする場合、アカウントキーを使用してサービスアカウント認証を実行します。

サービスアカウントにロールを付与し、アクセスするアカウントを切り替えることで BigQuery のアクセスが制御できるか確認してみます。

下図のようなイメージです。

サービスアカウントを作成

GCP 管理コンソールナビゲーションメニュー「IAM と管理」から、「サービスアカウント」を選択してサービスアカウントAを追加します。

「サービスアカウント名」と「サービスアカウントの説明」を入力し「作成」

アカウントの権限設定画面で「ロール」に「BigQuery 管理者」を選択し「続行」

「キーを作成」ボタンでアカウントキーの JSON ファイルをダウンロードして「完了」します。

サービスアカウントキーで BigQuery にアクセス

まず、認証情報の指定がない状態で、Python クライアントライブラリを使って BigQuery にアクセスしてみます。

Python 3.7.4 環境で、google-cloud-bigquery をインストールしました。

(test_account) [ec2-user@ip-10-0-43-239 ~]$ python --version
Python 3.7.4
(test_account) [ec2-user@ip-10-0-43-239 ~]$ pip -V
pip 19.0.3 from /home/ec2-user/test_account/lib64/python3.7/site-packages/pip (python 3.7)
(test_account) [ec2-user@ip-10-0-43-239 ~]$ pip install google-cloud-bigquery
Collecting google-cloud-bigquery
(省略)
Installing collected packages: six, google-resumable-media, protobuf, chardet, certifi, idna, urllib3, requests, googleapis-common-protos, pytz, pyasn1, pyasn1-modules, cachetools, rsa, google-auth, google-api-core, google-cloud-core, google-cloud-bigque           ry
  Running setup.py install for googleapis-common-protos ... done
Successfully installed cachetools-4.0.0 certifi-2019.11.28 chardet-3.0.4 google-api-core-1.16.0 google-auth-1.13.1 google-cloud-bigquery-1.24.0 google-cloud-core-1.3.0 google-resumable-media-0.5.0 googleapis-common-protos-1.51.0 idna-2.9 protobuf-3.11.3            pyasn1-0.4.8 pyasn1-modules-0.2.8 pytz-2019.3 requests-2.23.0 rsa-4.0 six-1.14.0 urllib3-1.25.8

クライアントライブラリを使用して API 経由で BigQuery にアクセスする場合、コードで認証情報を指定しないと、環境変数に設定されたデフォルトの認証情報でアクセスします。

現在、認証情報のデフォルトとなる、環境変数 GOOGLE_APPLICATION_CREDENTIALS は設定していない状態です。

(test_account) [ec2-user@ip-10-0-43-239 ~]$ echo $GOOGLE_APPLICATION_CREDENTIALS

(test_account) [ec2-user@ip-10-0-43-239 ~]$

ためしに以下の認証情報指定なしの Python コードを実行してみます。

from google.cloud import bigquery

query = (
    'SELECT name FROM `cm-da-mikami-yuki-258308.dataset_1.table_dogs`'
    'WHERE id = 1 ')

client = bigquery.Client()
query_job = client.query(query)
rows = query_job.result()
for row in rows:
    print(row.name)
(test_account) [ec2-user@ip-10-0-43-239 ~]$ python test_select.py
Traceback (most recent call last):
  File "test_select.py", line 3, in <module>
    client = bigquery.Client()
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/client.py", line 177, in __init__
    project=project, credentials=credentials, _http=_http
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/client.py", line 226, in __init__
    _ClientProjectMixin.__init__(self, project=project)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/client.py", line 178, in __init__
    project = self._determine_default(project)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/client.py", line 193, in _determine_default
    return _determine_default_project(project)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/_helpers.py", line 186, in _determine_default_project
    _, project = google.auth.default()
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/auth/_default.py", line 321, in default
    raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
google.auth.exceptions.DefaultCredentialsError: Could not automatically determine credentials. Please set GOOGLE_APPLICATION_CREDENTIALS or explicitly create credentials and re-run the application. For more information, please see https://cloud.google.com/docs/authentication/getting-started

認証エラーになりました。

続いて、先ほど作成したサービスアカウントAのアカウントキーファイルパスをコード内で指定してみます。

from google.cloud import bigquery
from google.oauth2 import service_account

key_path = '/home/ec2-user/test_account/key_account_a.json'
credentials = service_account.Credentials.from_service_account_file(
    key_path,
    scopes=["https://www.googleapis.com/auth/cloud-platform"],
)

query = (
    'SELECT name FROM `cm-da-mikami-yuki-258308.dataset_1.table_dogs`'
    'WHERE id = 1 ')

#client = bigquery.Client()
client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)
query_job = client.query(query)
rows = query_job.result()
for row in rows:
    print(row.name)
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select.py
シェパード

正常にアクセスすることができました。

なお、認証情報はファイルパスではなく、JSON オブジェクトで渡すことも可能です。

Python コードを以下に書き換えて実行してみます。

from google.cloud import bigquery
from google.oauth2 import service_account
import json

key_path = '/home/ec2-user/test_account/key_account_a.json'
#credentials = service_account.Credentials.from_service_account_file(
#    key_path,
#    scopes=["https://www.googleapis.com/auth/cloud-platform"],
#)
service_account_info = json.load(open(key_path))
credentials = service_account.Credentials.from_service_account_info(
    service_account_info)

query = (
    'SELECT name FROM `cm-da-mikami-yuki-258308.dataset_1.table_dogs`'
    'WHERE id = 3 ')

client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)
query_job = client.query(query)
rows = query_job.result()
for row in rows:
    print(row.name)
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_2.py
秋田犬

ファイルパスを指定した場合同様、正常にアクセスすることができました。

キーファイルをサーバ上に置くのはセキュリティ面が心配な場合など、ファイルは保護された別の場所に置いておいて、必要な時に read してアクセスするのも良いですね。

サービスアカウントを切り替えて BigQuery 操作権限を制御

もう一つ「BigQuery データ閲覧者」と「BigQuery ジョブユーザー」権限を持つ別のサービスアカウントBを作成しました。

Python 実行時のコマンドライン引数でファイル名を渡すように変更して、作成したアカウントBのキーファイルを指定して実行してみます。

from google.cloud import bigquery
from google.oauth2 import service_account
import argparse
import os.path

parser = argparse.ArgumentParser(description='select data')
parser.add_argument('file', help='account key file')
args = parser.parse_args()

#key_path = '/home/ec2-user/test_account/key_account_a.json'
key_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.file)
credentials = service_account.Credentials.from_service_account_file(
    key_path,
    scopes=["https://www.googleapis.com/auth/cloud-platform"],
)

query = (
    'SELECT id, name FROM `cm-da-mikami-yuki-258308.dataset_2.table_cats` '
    'ORDER BY id '
)

client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)
query_job = client.query(query)
rows = query_job.result()
for row in rows:
    print('{} : {}'.format(row.id, row.name))
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_arg.py key_account_b.json
1 : ベンガル
2 : ロシアンブルー
3 : 三毛猫

データが参照できることを確認して、次に、閲覧者権限しかないアカウントBのアカウントキーで、テーブルにデータを INSERT する以下のコードを実行してみます。

from google.cloud import bigquery
from google.oauth2 import service_account
import argparse
import os.path

parser = argparse.ArgumentParser(description='select data')
parser.add_argument('file', help='account key file')
args = parser.parse_args()

key_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.file)
credentials = service_account.Credentials.from_service_account_file(
    key_path,
    scopes=["https://www.googleapis.com/auth/cloud-platform"],
)

query = (
    "INSERT INTO `cm-da-mikami-yuki-258308.dataset_2.table_cats` VALUES (4, 'アビシニアン') "
)

client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)
query_job = client.query(query)
results = query_job.result()
(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_insert.py key_account_b.json
Traceback (most recent call last):
  File "test_insert.py", line 26, in <module>
    results = query_job.result()
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 3196, in result
    super(QueryJob, self).result(retry=retry, timeout=timeout)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 818, in result
    return super(_AsyncJob, self).result(timeout=timeout)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/api_core/future/polling.py", line 127, in result
    raise self._exception
google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_2.table_cats: User does not have bigquery.tables.updateData permission for table cm-da-mikami-yuki-258308:dataset_2.table_cats.

(job ID: 839d4b96-52e1-4795-b988-f9c8dfe8c0af)

                           -----Query Job SQL Follows-----

    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |
   1:INSERT INTO `cm-da-mikami-yuki-258308.dataset_2.table_cats` VALUES (4, 'アビシニアン')
    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |

指定したアカウントキーのサービスアカウントBには INSERT 権限がないのでエラーになりました。

INSERT も可能な管理者権限を付与したアカウントAのキーファイルに切り替えて再実行してみると

(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_insert.py key_account_a.json

エラーになることなく実行できました。

テーブルデータを確認してみます。

(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_arg.py key_account_b.json
1 : ベンガル
2 : ロシアンブルー
3 : 三毛猫
4 : アビシニアン

ちゃんとデータが INSERT できていることが確認できました。

サービスアカウントを切り替えてデータセットアクセスを制御

dataset_1dataset_2 の2つのデータセットがあるとして、

  • ユーザーBは dataset_1dataset_2 両方とも参照可能
  • ユーザーCは dataset_1 は参照できるが dataset_2 は参照できない

場合を想定しました。

「BigQuery ジョブユーザー」権限しか持っていない、別のサービスアカウントCを作成します。

BigQuery 管理画面の「共有データセット」から、dataset_1 に、アカウントCの BigQuery データ閲覧権限を追加しました。

一方、dataset_2 には、アカウントCのアクセス権を追加しませんでした。

dataset_1dataset_2 それぞれに対して、アカウントCのアクセスキーで SELECT を実行してみます。

(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_ds1.py key_account_c.json
1 : シェパード
2 : シベリアンハスキー
3 : 秋田犬

dataset_1 には正常にアクセスできます。 dataset_2 のデータも SELECT してみると・・・

(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_ds2.py key_account_c.json
Traceback (most recent call last):
  File "test_select_ds2.py", line 26, in <module>
    rows = query_job.result()
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 3196, in result
    super(QueryJob, self).result(retry=retry, timeout=timeout)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/cloud/bigquery/job.py", line 818, in result
    return super(_AsyncJob, self).result(timeout=timeout)
  File "/home/ec2-user/test_account/lib64/python3.7/site-packages/google/api_core/future/polling.py", line 127, in result
    raise self._exception
google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_2.table_cats: User does not have permission to query table cm-da-mikami-yuki-258308:dataset_2.table_cats.

(job ID: 10503ea7-dcb8-45bb-b219-8335cf4ee295)

                           -----Query Job SQL Follows-----

    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |
   1:SELECT id, name FROM `cm-da-mikami-yuki-258308.dataset_2.table_cats` ORDER BY id
    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |

アカウントCは dataset_2 の閲覧権限を持っていないので、permission エラーになりました。

dataset_2 閲覧権のあるアカウントBのキーファイルを指定して再実行してみると

(test_account) [ec2-user@ip-10-0-43-239 test_account]$ python test_select_ds2.py key_account_b.json
1 : ベンガル
2 : ロシアンブルー
3 : 三毛猫
4 : アビシニアン

アカウントキーを切り替えることで、付与された権限に従って BigQuery のアクセス制御ができることを確認できました。

まとめ(所感)

クライアントライブラリを使えばややこしい OAuth 2.0 の認証も自動でやってくれるので、アカウントキーファイルだけ意識しておけば良さそうです。

サービスアカウントへのロール付与と、BigQuery データセットの権限付与に気をつければ、サービスアカウントを切り替えることで、アプリなどからでも BigQuery の操作権限やデータセットのアクセス権限を簡単に制御することができました。

参考