BigQuery でテーブルレベルのアクセス制御(テーブル ACL )ができるようになったので挙動を確認してみた

2020.08.04

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

これまで BigQuery でアクセス制御できる最下層のリソースはデータセットでした。

先日、テーブル単位でアクセス権を設定することができるようになったので、挙動を確認してみました。

やりたいこと

  • テーブルレベルでアクセス制御(テーブル ACL )を設定するにはどうすればいいのか知りたい
  • テーブル ACL を設定した場合の挙動を確認したい
  • テーブルへのアクセス制御設定とデータセットなど他のリソースへのアクセス制御設定は競合しないのか確認したい
  • テーブル ACL は継承されることがあるのかどうか確認したい

前提

BigQuery のテーブルレベルのアクセス制御機能は、現在ベータ版とのことです。

動作検証では Python クライアントライブラリ経由( BigQuery API )で、サービスアカウントを使用してアクセス制御状態を確認しました。

bq コマンド、Python クライアントライブラリは、CLOUD SHELL から実行しました。

テーブルにアクセスポリシーを設定

以下の通り、検証用のサービスアカウントを作成しました。

BigQuery の管理コンソールからアクセス制御を設定したいテーブルを選択し、「テーブルを共有」ボタンをクリックします。

権限設定画面で「メンバーを追加」欄にアクセス制御を追加したいサービスアカウントを入力し、プルダウンから付与するロールを選択して「追加」をクリック。

table_sample テーブルに対して、2 つのサービスアカウントに BigQuery データオーナーと BigQuery データ閲覧者のロールを付与しました。

データセット / テーブルへのアクセス権を確認

以下の 4 つのサービスアカウントで、データセットやテーブルにアクセスできるかどうか確認してみます。

サービスアカウント ロール(to Dataset) ロール(to Table)
bq-data-owner BigQuery データオーナー BigQuery データオーナー(継承)
table-acl-data-owner なし BigQuery データオーナー
table-acl-data-viewer なし BigQuery データ閲覧者
table-acl-test なし なし

動作検証に使用するサービスアカウントキーを Cloud Shell にアップロードします。 Cloud Shell へのファイルアップロード / ダウンロードは、メニューから「アップロード」/「ダウンロード」を選択することで GUI 経由で簡単に実行できます。

データセット一覧を取得

まずは各サービスアカウントで、データセットにアクセスできるかどうか確認してみます。 table-acl-* で始まるサービスアカウントには、データセットへのアクセス権は付与していません。

パラメータでサービスアカウントキーファイルを指定して、BigQuery のデータセット情報を取得する以下の Python コードを準備しました。

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

parser = argparse.ArgumentParser(description='project')
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"],
)
client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)

datasets = list(client.list_datasets())
project = client.project

if datasets:
    print("Datasets in project {}:".format(project))
    for dataset in datasets:
        print("\t{}".format(dataset.dataset_id))
else:
    print("{} project does not contain any datasets.".format(project))

まずはデータセットへのアクセス権があるサービスアカウントを指定して実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 list_dataset.py keys/bq-data-owner.json
Datasets in project cm-da-mikami-yuki-258308:
(省略)
        dataset_1
        dataset_2
        load_from_gcs
(省略)

データセットアクセスできることが確認できました。

続いてデータセットへのアクセス権を付与していないサービスアカウントで実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 list_dataset.py keys/table-acl-data-owner.json
cm-da-mikami-yuki-258308 project does not contain any datasets.

指定したロールの通り、データセットにはアクセスできないことが確認できました。

テーブル一覧を取得

以下の Python コードで、テーブル一覧を取得してみます。

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

parser = argparse.ArgumentParser(description='project')
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"],
)
client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)

dataset_id = '{}.dataset_1'.format(credentials.project_id)
tables = client.list_tables(dataset_id)

print("Tables contained in '{}':".format(dataset_id))
for table in tables:
    print("{}.{}.{}".format(table.project, table.dataset_id, table.table_id))

データセットおよび配下の全てのテーブルへのアクセス権があるサービスアカウントで実行すると

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 list_table.py keys/bq-data-owner.json
Tables contained in 'cm-da-mikami-yuki-258308.dataset_1':
cm-da-mikami-yuki-258308.dataset_1.animals
(省略)
cm-da-mikami-yuki-258308.dataset_1.table_sample
cm-da-mikami-yuki-258308.dataset_1.table_sample_encrypt
cm-da-mikami-yuki-258308.dataset_1.table_sample_encrypt_copy
cm-da-mikami-yuki-258308.dataset_1.table_sample_partition
cm-da-mikami-yuki-258308.dataset_1.table_sample_partition_copy
cm-da-mikami-yuki-258308.dataset_1.test_create
cm-da-mikami-yuki-258308.dataset_1.view_dogs
cm-da-mikami-yuki-258308.dataset_1.view_dogs_2
cm-da-mikami-yuki-258308.dataset_1.view_sample

データセット配下のテーブル一覧を取得できます。

続いて、データセット配下の特定のテーブル( table_sample )にのみ BigQuery データオーナーのロールを付与したサービスアカウントで実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 list_table.py keys/table-acl-data-owner.json
Tables contained in 'cm-da-mikami-yuki-258308.dataset_1':
Traceback (most recent call last):
  File "list_table.py", line 23, in <module>
    for table in tables:
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/page_iterator.py", line 212, in _items_iter
    for page in self._page_iter(increment=False):
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/page_iterator.py", line 243, in _page_iter
    page = self._next_page()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/page_iterator.py", line 369, in _next_page
    response = self._get_next_page_response()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/page_iterator.py", line 419, in _get_next_page_response
    method=self._HTTP_METHOD, path=self.path, query_params=params
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 574, in _call_api
    return call()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 286, in retry_wrapped_func
    on_error=on_error,
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 184, in retry_target
    return target()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/_http.py", line 423, in api_request
    raise exceptions.from_http_response(response)
google.api_core.exceptions.Forbidden: 403 GET https://bigquery.googleapis.com/bigquery/v2/projects/cm-da-mikami-yuki-258308/datasets/dataset_1/tables: Access Denied: Dataset cm-da-mikami-yuki-258308:dataset_1: User does not have bigquery.tables.list permission for dataset cm-da-mikami-yuki-258308:dataset_1.

User does not have bigquery.tables.list permission とのことで、テーブルリストは取得できませんでした。

BigQuery の事前定義ロール「BigQuey データオーナー」には bigquery.tables.list 権限も付与されているはずですが、特定のテーブルのみへのロール付与の場合は対象のテーブル含めて一覧の取得はできませんでした。

テーブル情報を取得

以下の Python コードで、テーブル情報を取得してみます。

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

parser = argparse.ArgumentParser(description='project')
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"],
)
client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)

table_id = '{}.dataset_1.table_sample'.format(credentials.project_id)
table = client.get_table(table_id)

print(
    "Got table '{}.{}.{}'.".format(table.project, table.dataset_id, table.table_id)
)
print("Table schema: {}".format(table.schema))
print("Table description: {}".format(table.description))
print("Table has {} rows".format(table.num_rows))

まずはデータセットおよびテーブルに BigQuery データオーナーのロールが付与されているサービスアカウントで実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 get_table.py keys/bq-data-owner.json
Got table 'cm-da-mikami-yuki-258308.dataset_1.table_sample'.
Table schema: [SchemaField('name', 'STRING', 'REQUIRED', '名前', (), None), SchemaField('gender', 'STRING', 'NULLABLE', '性別', (), None), SchemaField('count', 'INTEGER', 'NULLABLE',
 '人数', (), None)]
Table description: テーブルコピーのテスト用
Table has 34073 rows

テーブル情報を取得することができました。

続いて、テーブルに対してのみロールを付与したサービスアカウントで実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 get_table.py keys/table-acl-data-owner.json
Got table 'cm-da-mikami-yuki-258308.dataset_1.table_sample'.
Table schema: [SchemaField('name', 'STRING', 'REQUIRED', '名前', (), None), SchemaField('gender', 'STRING', 'NULLABLE', '性別', (), None), SchemaField('count', 'INTEGER', 'NULLABLE',
 '人数', (), None)]
Table description: テーブルコピーのテスト用
Table has 34073 rows
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 get_table.py keys/table-acl-data-viewer.json
Got table 'cm-da-mikami-yuki-258308.dataset_1.table_sample'.
Table schema: [SchemaField('name', 'STRING', 'REQUIRED', '名前', (), None), SchemaField('gender', 'STRING', 'NULLABLE', '性別', (), None), SchemaField('count', 'INTEGER', 'NULLABLE',
 '人数', (), None)]
Table description: テーブルコピーのテスト用
Table has 34073 rows

BigQuery データオーナー、BigQuery データ閲覧者どちらのロールでも、アクセス許可したテーブルであれば、テーブル情報が参照できることが確認できました。

なお、アクセス権を設定していない別のテーブル情報を取得しようとしてみると

(省略)
#table_id = '{}.dataset_1.table_sample'.format(credentials.project_id)
table_id = '{}.dataset_1.table_sample_partition'.format(credentials.project_id)
table = client.get_table(table_id)
(省略)
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 get_table.py keys/table-acl-data-owner.json
Traceback (most recent call last):
  File "get_table.py", line 21, in <module>
    table = client.get_table(table_id)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 697, in get_table
    retry, method="GET", path=table_ref.path, timeout=timeout
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 574, in _call_api
    return call()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 286, in retry_wrapped_func
    on_error=on_error,
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 184, in retry_target
    return target()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/_http.py", line 423, in api_request
    raise exceptions.from_http_response(response)
google.api_core.exceptions.Forbidden: 403 GET https://bigquery.googleapis.com/bigquery/v2/projects/cm-da-mikami-yuki-258308/datasets/dataset_1/tables/table_sample_partition: Access Denied: Table cm-da-mikami-yuki-258308:dataset_1.table_sample_partition: User does not have bigquery.tables.get permission for table cm-da-mikami-yuki-258308:dataset_1.table_sample_partition.

アクセス制御設定の通り、アクセスを許可していないテーブル情報は取得できません。

さらに、データセットにもテーブルにもアクセス権を設定していないサービスアカウントで実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 get_table.py keys/table-acl-test.json
Traceback (most recent call last):
  File "get_table.py", line 20, in <module>
    table = client.get_table(table_id)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 697, in get_table
    retry, method="GET", path=table_ref.path, timeout=timeout
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 574, in _call_api
    return call()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 286, in retry_wrapped_func
    on_error=on_error,
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 184, in retry_target
    return target()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/_http.py", line 423, in api_request
    raise exceptions.from_http_response(response)
google.api_core.exceptions.Forbidden: 403 GET https://bigquery.googleapis.com/bigquery/v2/projects/cm-da-mikami-yuki-258308/datasets/dataset_1/tables/table_sample: Access Denied: Table cm-da-mikami-yuki-258308:dataset_1.table_sample: User does not have bigquery.tables.get permission for table cm-da-mikami-yuki-258308:dataset_1.table_sample.

こちらも設定の通り、許可していないサービスアカウントからはアクセスできないことが確認できました。

テーブルデータへのアクセス権を確認

アクセス制御を設定したテーブルに対して、SQL を実行してデータアクセス時の挙動を確認してみます。

Python クライアントライブラリ経由で SQL クエリを実行する場合はジョブ実行となりますが、検証対象の BigQuery データオーナー、BigQuery データ閲覧者ロールにはどちらも bigquery.jobs.create 権限がないため、ジョブを実行できません。

GCP 管理コンソールナビゲーションメニュー「IAM と管理」から「IAM」を選択し、検証するサービスアカウントに「BigQuery ジョブユーザー」ロールを付与して IAM を追加しました。

テーブルデータの操作

以下のサービスアカウントで、テーブルデータの参照とレコード追加・更新・削除処理の可否を確認してみます。

サービスアカウント ロール 参照 追加 / 更新 / 削除
table-acl-data-owner BigQuery データオーナー
table-acl-data-viewer BigQuery データ閲覧者 不可

まずは参照、追加、更新、削除全ての操作が可能な BigQuery データオーナーロールを付与したサービスアカウントで、以下の Python コードを実行してみます。

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

parser = argparse.ArgumentParser(description='project')
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"],
)
client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)

query = 'SELECT name FROM dataset_1.table_sample ORDER BY count LIMIT 3'
query_job = client.query(query)
results = query_job.result()
for row in results:
    print("name: {}".format(row.name))

query = 'INSERT INTO dataset_1.table_sample VALUES("Yuki", "F", 1)'
query_job = client.query(query)
results = query_job.result()
print("insert comp.")

query = 'UPDATE dataset_1.table_sample SET count = 2 WHERE name = "Yuki"'
query_job = client.query(query)
results = query_job.result()
print("update comp.")

query = 'SELECT name, count FROM dataset_1.table_sample WHERE name = "Yuki"'
query_job = client.query(query)
results = query_job.result()
for row in results:
    print("name: {}, count: {}".format(row.name, row.count))

query = 'DELETE FROM dataset_1.table_sample WHERE name = "Yuki"'
query_job = client.query(query)
results = query_job.result()
print("delete comp.")
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query.py keys/table-acl-data-owner.json 
name: Kisa
name: Faylynn
name: Quynh
insert comp.
update comp.
name: Yuki, count: 2
delete comp.

全ての操作が問題なく実行できることが確認できました。

続いて参照権しかない BigQuery データ閲覧者ロールのサービスアカウントを指定して実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query.py keys/table-acl-data-viewer.json
name: Kisa
name: Faylynn
name: Quynh
Traceback (most recent call last):
  File "query.py", line 27, in <module>
    results = query_job.result()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 3207, in result
    super(QueryJob, self).result(retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 812, in result
    return super(_AsyncJob, self).result(timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/future/polling.py", line 130, in result
    raise self._exception
google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_1.table_sample: User does not have bigquery.tables.updateData permission for table cm-da-mikami-yuki-258308:dataset_1.table_sample.
(job ID: 88da6965-414d-4737-a7c0-5fe34210b69d)
               -----Query Job SQL Follows-----
    |    .    |    .    |    .    |    .    |    .    |
   1:INSERT INTO dataset_1.table_sample VALUES("Yuki", "F", 1)
    |    .    |    .    |    .    |    .    |    .    |
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$

設定したロールの通り、参照のみ可能なことが確認できました。

テーブルに対して BigQuery ジョブ実行権限は付与できる?

BigQuery ジョブユーザーロールが付与されていない状態で Python クライアントライブラリから SQL を実行しようとすると、以下の通り bigquery.jobs.create 権限エラーが発生します。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query.py keys/table-acl-data-owner.json
Traceback (most recent call last):
  File "query.py", line 20, in <module>
    query_job = client.query(query)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 2471, in query
    query_job._begin(retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 3156, in _begin
    super(QueryJob, self)._begin(client=client, retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 638, in _begin
    retry, method="POST", path=path, data=self.to_api_repr(), timeout=timeout
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 574, in _call_api
    return call()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 286, in retry_wrapped_func
    on_error=on_error,
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 184, in retry_target
    return target()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/_http.py", line 423, in api_request
    raise exceptions.from_http_response(response)
google.api_core.exceptions.Forbidden: 403 POST https://bigquery.googleapis.com/bigquery/v2/projects/cm-da-mikami-yuki-258308/jobs: Access Denied: Project cm-da-mikami-yuki-258308: User does not have bigquery.jobs.create permission in project cm-da-mikami-yuki-258308.
(job ID: 44b3d65c-0cfd-4376-a947-2b941b4c0751)
                  -----Query Job SQL Follows-----
    |    .    |    .    |    .    |    .    |    .    |    .    |
   1:SELECT name FROM dataset_1.table_sample ORDER BY count LIMIT 3
    |    .    |    .    |    .    |    .    |    .    |    .    |

BigQuery 管理コンソールから、テーブルに対して BigQuery ジョブジョブユーザーのロールを付与しようとしても、プルダウンにジョブユーザーのロールが表示されていないため選択できません。

テーブルに対するアクセス権設定は、bq コマンドでも実行できます。

現在のポリシーを取得します。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ bq get-iam-policy --format=prettyjson \
> cm-da-mikami-yuki-258308:dataset_1.table_sample \
>  > policy.json

以下のポリシーファイルが取得できました。

{
  "bindings": [
    {
      "members": [
        "serviceAccount:table-acl-data-owner@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataOwner"
    },
    {
      "members": [
        "serviceAccount:table-acl-data-viewer@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataViewer"
    }
  ],
  "etag": "BwWr9k9uSBw=",
  "version": 1
}

上記ファイルに roles/bigquery.jobUser ロールを追記しました。

{
  "bindings": [
    {
      "members": [
        "serviceAccount:table-acl-data-owner@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataOwner"
    },
    {
      "members": [
        "serviceAccount:table-acl-data-viewer@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataViewer"
    },
    {
      "members": [
        "serviceAccount:table-acl-data-viewer@cm-da-mikami-yuki-258308.iam.gserviceaccount.com",
        "serviceAccount:table-acl-data-owner@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.jobUser"
    }
  ],
  "etag": "BwWr9k9uSBw=",
  "version": 1
}

ポリシーを更新しようとすると

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ bq set-iam-policy \
>  cm-da-mikami-yuki-258308:dataset_1.table_sample \
>  policy.json
BigQuery error in set-iam-policy operation: IAM setPolicy failed for Table cm-da-mikami-yuki-258308:dataset_1.table_sample: Role roles/bigquery.jobUser is not supported for this resource.

Role roles/bigquery.jobUser is not supported for this resource. とのことで、テーブルに対して BigQuery ジョブユーザーロールは付与できませんでした。

BigQuery の事前定義済みロールがどのリソースに対して設定できるかは、以下のドキュメント「最下位のリソース」欄で確認できます。

BigQuery ジョブユーザーロール( roles/bigquery.jobUser )が付与できる最下位のリソースは「プロジェクト」とのことなので、データセットやテーブルには付与できません。

一方、データセットやテーブルには、カスタムロールを設定することができます。

では、ジョブ実行可能な bigquery.jobs.create 権限を持つカスタムロールを作成し、そのカスタムロールをテーブル ACL で設定すればジョブ実行できるのでしょうか?

BigQuery データ閲覧者ロールの権限 + bigquery.jobs.create 権限を付与したカスタムロールを作成し、テーブルの権限設定画面からカスタムロールでサービスアカウントを追加しました。

カスタムロールで追加したサービスアカウントを指定して、以下の SELECT SQL を実行してみます。

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

parser = argparse.ArgumentParser(description='project')
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"],
)
client = bigquery.Client(
    credentials=credentials,
    project=credentials.project_id,
)

query = 'SELECT name FROM dataset_1.table_sample ORDER BY count LIMIT 3'
query_job = client.query(query)
results = query_job.result()
for row in results:
    print("name: {}".format(row.name))
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query_select.py keys/table-acl-custom-viewer.json
Traceback (most recent call last):
  File "query_select.py", line 20, in <module>
    query_job = client.query(query)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 2471, in query
    query_job._begin(retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 3156, in _begin
    super(QueryJob, self)._begin(client=client, retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 638, in _begin
    retry, method="POST", path=path, data=self.to_api_repr(), timeout=timeout
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/client.py", line 574, in _call_api
    return call()
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 286, in retry_wrapped_func
    on_error=on_error,
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/retry.py", line 184, in retry_target
    return target()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/_http.py", line 423, in api_request
    raise exceptions.from_http_response(response)
google.api_core.exceptions.Forbidden: 403 POST https://bigquery.googleapis.com/bigquery/v2/projects/cm-da-mikami-yuki-258308/jobs: Access Denied: Project cm-da-mikami-yuki-258308: User does not have bigquery.jobs.create permission in project cm-da-mikami-yuki-258308.

(job ID: d119cdfd-6289-4083-a81b-6b250c1e4e19)

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

    |    .    |    .    |    .    |    .    |    .    |    .    |
   1:SELECT name FROM dataset_1.table_sample ORDER BY count LIMIT 3
    |    .    |    .    |    .    |    .    |    .    |    .    |

ジョブはプロジェクトに関連付けられるため、テーブルに対してのみ権限を付与した状態では、やはり実行できませんでした。

データセットとテーブルのアクセス制御はどちらが優先?

BigQuery では、データセットに対してもアクセス権を設定できます。

では、データセットとテーブル両方に対して、異なる権限を付与した場合、どちらのアクセス制御が優先されるのでしょうか?

データセットに BigQuery データ閲覧者 ロール、テーブルに BigQuery データオーナーロールを付与したサービスアカウントで、テーブルデータを編集できるかどうか確認してみます。

(省略)
query = 'SELECT name FROM dataset_1.table_sample ORDER BY count LIMIT 3'
query_job = client.query(query)
results = query_job.result()
for row in results:
    print("name: {}".format(row.name))

query = 'INSERT INTO dataset_1.table_sample VALUES("Yuki", "F", 1)'
query_job = client.query(query)
results = query_job.result()
print("insert comp.")

query = 'UPDATE dataset_1.table_sample SET count = 2 WHERE name = "Yuki"'
query_job = client.query(query)
results = query_job.result()
print("update comp.")

query = 'SELECT name, count FROM dataset_1.table_sample WHERE name = "Yuki"'
query_job = client.query(query)
results = query_job.result()
for row in results:
    print("name: {}, count: {}".format(row.name, row.count))

query = 'DELETE FROM dataset_1.table_sample WHERE name = "Yuki"'
query_job = client.query(query)
results = query_job.result()
print("delete comp.")
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query.py keys/table-acl-data-owner.json
name: Kisa
name: Faylynn
name: Quynh
insert comp.
update comp.
name: Yuki, count: 2
delete comp.

SELECTINSERTUPDATEDELETE いずれの SQL も、問題なく実行できました。

逆に、データセットに BigQuery データオーナーロール、テーブルに BigQuery データ閲覧者ロールを付与したサービスアカウントで確認してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query.py keys/table-acl-data-viewer.json
name: Kisa
name: Faylynn
name: Quynh
insert comp.
update comp.
name: Yuki, count: 2
delete comp.

こちらも問題なく全ての SQL が実行できることが確認できました。

Cloud IAM のアクセス モデルでは、権限は追加型です。リソースの権限は、ポリシー階層で説明されているように、親リソースから継承されます。リソースに追加された権限によって、追加のアクセス権が付与されます。テーブル ACL では追加のアクセス権の付与ができるだけで、データセットや Google Cloud プロジェクトのアクセス権は削除できません。

「権限は追記型」とのことで、プロジェクト、データセット、テーブルなど複数のリソースに対してアクセス制御設定を行っている場合には、一番強い権限が適用されるようです。

テーブルをコピーしたらアクセスポリシーもコピーされる?

アクセス制御設定済みのテーブルをコピーした場合、コピー先のテーブルにもテーブル ACL が継承されるかどうか確認してみます。

コピー元テーブルのアクセスポリシーは以下の通りです。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ bq get-iam-policy --format=prettyjson \
>  cm-da-mikami-yuki-258308:dataset_1.table_sample
{
  "bindings": [
    {
      "members": [
        "serviceAccount:table-acl-data-owner@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataOwner"
    },
    {
      "members": [
        "serviceAccount:table-acl-data-viewer@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataViewer"
    }
  ],
  "etag": "BwWsCNv+ccY=",
  "version": 1
}

テーブルをコピーして、コピー先テーブルのアクセスポリシーを確認してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ bq cp dataset_1.table_sample dataset_1.table_sample_copy
Waiting on bqjob_r22259c341760c060_00000173b88be49b_1 ... (0s) Current status: DONE   
Table 'cm-da-mikami-yuki-258308:dataset_1.table_sample' successfully copied to 'cm-da-mikami-yuki-258308:dataset_1.table_sample_copy'
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ bq get-iam-policy --format=prettyjson \
>  cm-da-mikami-yuki-258308:dataset_1.table_sample_copy
{
  "etag": "ACAB"
}

ポリシーはコピーされていません。

念のため、コピー先テーブルに対して SQL を実行してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query_select.py keys/table-acl-data-owner.json
Traceback (most recent call last):
  File "query_select.py", line 22, in <module>
    results = query_job.result()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 3207, in result
    super(QueryJob, self).result(retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 812, in result
    return super(_AsyncJob, self).result(timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/future/polling.py", line 130, in result
    raise self._exception
google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_1.table_sample_copy: User does not have permission to query table cm-da-mikami-yuki-258308:dataset_1.table_sample_copy.
(job ID: 35b50984-7b08-48ef-9b07-fa4a98ca6c2f)
                    -----Query Job SQL Follows-----
    |    .    |    .    |    .    |    .    |    .    |    .    |
   1:SELECT name FROM dataset_1.table_sample_copy ORDER BY count LIMIT 3
    |    .    |    .    |    .    |    .    |    .    |    .    |
gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$

コピー先のテーブルに対するアクセス権は設定されていないため、データも参照できません。

テーブルコピーではアクセス権は継承されないため、テーブル ACL を設定している場合にテーブル名変更などでテーブルコピーを実行する時には、アクセス権も再設定する必要があります。

アクセス可能なテーブルをソースとするビューにはアクセスできる?

以下の SQL で、table_sample をソーステーブルとするビューを作成しました。

CREATE VIEW cm-da-mikami-yuki-258308.dataset_1.v_sample AS
SELECT name, count FROM dataset_1.table_sample WHERE gender = 'M';

table_sample のアクセスポリシーは以下の通りです。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ bq get-iam-policy --format=prettyjson \
>  cm-da-mikami-yuki-258308:dataset_1.table_sample
{
  "bindings": [
    {
      "members": [
        "serviceAccount:table-acl-data-owner@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataOwner"
    },
    {
      "members": [
        "serviceAccount:table-acl-data-viewer@cm-da-mikami-yuki-258308.iam.gserviceaccount.com"
      ],
      "role": "roles/bigquery.dataViewer"
    }
  ],
  "etag": "BwWsCNv+ccY=",
  "version": 1
}

table_sample に BigQuery データ閲覧者ロールを設定済みの table-acl-data-viewer のサービスアカウントで、新しく作成したビューにもアクセスできるか確認してみます。

gcp_da_user@cloudshell:~ (cm-da-mikami-yuki-258308)$ python3 query_select.py keys/table-acl-data-viewer.json
Traceback (most recent call last):
  File "query_select.py", line 23, in <module>
    results = query_job.result()
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 3207, in result
    super(QueryJob, self).result(retry=retry, timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/cloud/bigquery/job.py", line 812, in result
    return super(_AsyncJob, self).result(timeout=timeout)
  File "/usr/local/lib/python3.7/dist-packages/google/api_core/future/polling.py", line 130, in result
    raise self._exception
google.api_core.exceptions.Forbidden: 403 Access Denied: Table cm-da-mikami-yuki-258308:dataset_1.v_sample: User does not have permission to query table cm-da-mikami-yuki-258308:dataset_1.v_sample.
(job ID: 685e3508-792f-401c-9473-396a5c231b58)
                -----Query Job SQL Follows-----
    |    .    |    .    |    .    |    .    |    .    |
   1:SELECT name FROM dataset_1.v_sample ORDER BY count LIMIT 3
    |    .    |    .    |    .    |    .    |    .    |

テーブルのアクセスポリシーの適用範囲は対象テーブルに限定され、ビューには適用されないことが確認できました。 承認済みビュー機能からも分かるように、ユーザー(サービスアカウント)の権限と、ソーステーブルからデータを SELECT するビューの権限は別管理となります。

テーブル ACL でアクセス設定済みのユーザーにビューのアクセス権も付与したい場合は、ビューに対にてもテーブル同様アクセスポリシーを設定するか、承認済みビューとして別のアクセス可能なデータセットにビューを作成する必要があります。

まとめ(所感)

テーブルレベルのアクセス制御により、BigQuery データに対してこれまでよりも細やかなアクセスコントロールが可能になりました。

テーブルコピーやビューの作成ではソーステーブルのアクセスポリシーは継承されないことが確認できたので、意図していないユーザーにデータが公開されるような問題も発生しないはずです。

現在まだベータ版ですが、テーブルレベルのアクセス制御がサポートされたことにより、特に他データベースサービスからの移行時などにはより使いやすくなるのではないでしょうか?

参考