【How-to Guides】GCP 踩坑日常 — 如何操控 IAM conditin 細化 prefix 不同權限

How to grant access to specific Cloud Storage buckets using IAM conditions.

Kellen
13 min readNov 2, 2023

如果在同一專案下有多個 Cloud Storage 儲存桶,則該儲存物件管理員也會被授予存取相同專案內其他儲存桶中資料的權限。如果公司的資料合規性政策需要比這更細粒度的存取控制,可以使用 Google Cloud IAM 條件將服務帳號權限限制僅特定儲存桶。而這也是 GCP 有提到的 IAM condition 的功能,翻看 GCP 文件是說只要設定 IAM condition 即可達成,疏不知自己動手作下去卻是排障的開始。

踩坑設定參考

期待是可以只限制在此 Bucket 且特定 prefix 下(limit prefix),可以達成物件上傳的動作,而其他的 prefix 則無法達成,設定如下:

但是,遇到了以下異常

$ gsutil cp local-file.txt gs://temp-demolab-bucket/limit/remote-file.txt
AccessDeniedException: 403 my-service-account@fubar.iam.gserviceaccount.com
does not have storage.objects.list access to the Google Cloud Storage bucket.
Permission 'storage.objects.list' denied on resource (or it may not exist).

上傳個物件,也與 storage.objects.list 有關?

幾翻查找,GCP 文件沒有這類設定的整合說明,只有提到可以透過 IAM condition 進行權限的設定,還好 Stakeflow 有大神說明讓大家不要在花時間撞牆(beating head against this wall)

先來看看發生什麼原因,而導致異常!

弄巧成拙 — 服務帳號給予單一角色並套用 IAM condition 影響到 list 功能

Google Cloud Storage(GCS) 中的權限控制是基於儲存桶(bucket)層級和物件(object)層級進行管理的。權限控制包括各種操作,例如讀取(list)、寫入、刪除物件等。其中在 GCS 中,「list」權限是一個特殊的權限,它用於確定使用者是否可以查看儲存桶中的物件清單。

IAM Conditions 可以使用條件來設定特定規則,以便控制權限何時授予或拒絕。增加對存儲桶和物件的精確控制非常有用。但是,條件若涉及「list」權限時,情況有些不同。

  • 「list」權限在儲存桶層級授予,無法限制只導入特定物件。
  • 使用 IAM Conditions 會限制住了「list」在 Bucket 層級的作用

好死不死 — 使用 gsutil cp 指令會使用到 storage.object.list 功能

使用gsutil cp命令將本機檔案上傳到 Google Cloud 儲存桶時,storage.objects.list權限通常不會直接使用。此權限是用於列示儲存桶中的物件,上傳檔案應該要使用 storage.objects.create 權限就好。

But ~~ 當執行gsutil cp命令時,實際上會涉及以下步驟:

  1. gsutil首先需要檢查儲存桶中是否存在同名檔案。為了執行此操作,需要檢查儲存桶中的對象,以檢查是否已經存在相同名稱的物件,這需要storage.objects.list權限
  2. 如果不存在同名文件,gsutil將本機文件上傳到儲存桶,這需要storage.objects.create權限。

排障錯亂 — 使用 Python 上傳物件則會使用到 storage.bucket.get 功能

後來改用 Python 測試物件上傳則是得到不同的結果(下圖),但程式碼中並沒有直接使用應該是不會觸發 storage.buckets.get 操作。這可能是在初始化 GCS Client 時,會執行一些預檢查操作以確保儲存桶是否存在,這可能觸發對儲存桶的檢查。

因此,要解決'storage.buckets.get' denied錯誤,需要確保服務帳戶my-service-account@fubar.iam.gserviceaccount.com具有適當的權限,以執行這些預檢查操作。

調整改善參考

(黃框)「list」權限是授予在儲存桶層級

作法參考:可以設計成指派兩個角色給服務帳號使用

  • (黃框)Storage Object Viewer(具有讀取物件權限)

這邊我選擇【建立】自訂角色,[新增權限] 上給予 storage.buckets.getstorage.buckets.list & storage.objects.list,或是可以直接選用有 Storage Object Viewer 角色使用更簡單。

  • (灰框)Storage Object Creator or Admin(建立物件)+ IAM condition

並於以上角色進行 IAM condition 規則編寫

resource.name.startsWith("projects/_/buckets/temp-demolab-bucket/objects/limit")

驗證結果(gsutil cp)

只能從 limit prefix 上傳物件,若上傳到其他的 prefix 會被阻擋,並告知 storage.objects.create 權限異常

# 驗證上傳至 /limit prefix 應該要能成功了
$ gsutil cp local-file.txt gs://temp-demolab-bucket/limit/remote-file.txt
Copying file://local-file.txt [Content-Type=text/plain]...
/ [1 files][ 0.0 B/ 0.0 B]
Operation completed over 1 objects.

# 驗證上傳至 非 /limit prefix ,如 /test 應該要失敗
$ gsutil cp local-file.txt gs://temp-demolab-bucket/test/remote-file.txt
Copying file://local-file.txt [Content-Type=text/plain]...
AccessDeniedException: 403 my-service-account@fubar.iam.gserviceaccount.com
does not have storage.objects.create access to the Google Cloud Storage object.
Permission 'storage.objects.create' denied on resource (or it may not exist).

驗證結果(Python)

  • 驗證上傳至 /limit prefix ,應該要成功
  • 驗證上傳至 非 /limit prefix ,如 /test 應該要失敗

Python Code 參考

import logging
from google.cloud import storage
from google.cloud.exceptions import NotFound, GoogleCloudError
import subprocess


def set_gcloud_project(project_id):
command = ['gcloud', 'config', 'set', 'project', project_id]
try:
subprocess.run(command, check=True)
print(f'Successfully set Google Cloud project to: {project_id}')
except subprocess.CalledProcessError as e:
print(f'Error setting Google Cloud project: {e}')

# 要設定的 Google Cloud 項目 ID
project_id = 'fubar'

# 執行設定命令
set_gcloud_project(project_id)


class GCSTools:
def __init__(self, bucket_name):
self.bucket_name = bucket_name
self.client = storage.Client()
self.bucket = self.client.get_bucket(bucket_name)
self.logger = logging.getLogger(__name__)

def upload_file(self, local_file_path, remote_file_name):
try:
blob = self.bucket.blob(remote_file_name)
blob.upload_from_filename(local_file_path)
self.logger.info(f'File {local_file_path} uploaded to GCS bucket {self.bucket_name}.')
except NotFound:
self.logger.error(f'Bucket {self.bucket_name} not found.')
except GoogleCloudError as e:
self.logger.error(f'An error occurred: {e}')

# 設置日誌級別和格式
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 創建 GCSTools 實例,指定 GCS 存儲桶名稱
bucket_name = 'temp-demolab-bucket'
gcs_uploader = GCSTools(bucket_name)


# 調用 upload_file 方法上傳文件
local_file_path = 'local-file.txt'
remote_file_name = 'test/remote-file.txt'
# remote_file_name = 'demo/remote-file.txt'
gcs_uploader.upload_file(local_file_path, remote_file_name)

其他 FAQ

  • 為何想使用 IAM condition 來細化權限

在尋找 GCS 的存取限制時,也有使用引入了在 GCS 直接在 Bucket 側新增權限的方法,雖然這種方法很簡單,但是很難理解哪些使用者和服務帳戶被允許存取哪些 GCS Bucket,權限部份就統一回到 IAM 對之後的維運或報表自動化應該會有幫助及一定程度的維運減輕。

  • 若只想控制在 Bucket 層即可,不用在細分到 prefix 控管

調整 Storage Admin / Creator 角色 IAM condition;而 Storage Viewer 不調整。

resource.name.startsWith("projects/_/buckets/temp-demolab-bucket/objects")
  • 可以不要讓服務帳號綁兩個角色這麼麻煩,希望可以使用一個角色與 IAM condition 來處理

行,原先目的是 Viewer 在組織權限最低權限,引入 Storage Admin / Creator 進行 IAM condition 以進行區別,當然也可以直接整合一個角色作業,缺點是權限設計是需要細到 CEL 上去解讀的。

resource.name == "projects/_/buckets/temp-demolab-bucket" ||
resource.name.startsWith("projects/_/buckets/temp-demolab-bucket/objects")

例如直接使用 Object Storage Admin 並於 IAM condition 加入上面的寫法,resource.name == "projects/_/buckets/temp-demolab-bucket"這個條件表示只有當資源名稱精確匹配時可以保留 get, list 在 Bucket 層的權限。但是,當你嘗試在儲存桶中建立物件時,這個條件並不會滿足,因此你可能沒有權限來建立物件。若要想要允許建立物件,需要設定條件(多了 OR 條件)來包含儲存桶中的物件,在條件中使用來包含儲存桶中的物件,一來,滿足使用者條件的將能夠建立物件。resource.name.startsWith("projects/_/buckets/temp-demolab-bucket/objects/")

--

--

Kellen

Backend(Python)/K8s and Container eco-system/Technical&Product Manager/host Developer Experience/早期投入資料創新與 ETL 工作,近期研究 GCP/Azure/AWS 相關的解決方案的 implementation