限制憑證的 Cloud Storage 權限

本頁說明如何使用憑證存取權界線,downscope或限制短期憑證可用的 Identity and Access Management (IAM) 權限。

您可以使用憑證存取範圍產生 OAuth 2.0 存取權杖,代表服務帳戶,但權限比服務帳戶少。舉例來說,如果某位客戶需要存取您控管的 Cloud Storage 資料,您可以採取下列做法:

  1. 建立可存取您擁有的每個 Cloud Storage bucket 的服務帳戶。
  2. 為服務帳戶產生 OAuth 2.0 存取權杖。
  3. 套用憑證存取邊界,只允許存取包含客戶資料的 bucket。

憑證存取權範圍的運作方式

如要縮小權限範圍,請定義憑證存取權範圍,指定短期憑證可存取的資源,以及每個資源可用的權限上限。接著,您可以建立短期憑證,然後換取符合憑證存取邊界的憑證。

如果您需要為每個工作階段授予主體一組不同的權限,使用憑證存取權界線會比建立多個不同的服務帳戶,並為每個服務帳戶授予一組不同的角色更有效率。

憑證存取權範圍的元件

憑證存取權範圍是包含存取權範圍規則清單的物件。每項規則都包含下列資訊:

  • 規則套用的資源。
  • 該資源可用的權限上限。
  • 選用:進一步限制權限的條件。條件包括:
    • 評估結果為 truefalse 的條件運算式。如果評估結果為 true,系統會允許存取;否則會拒絕存取。
    • 選填:識別條件的標題。
    • 選填:提供條件的詳細說明。

如果將憑證存取權範圍套用至短期憑證,則該憑證只能存取憑證存取權範圍內的資源。其他資源沒有權限。

憑證存取權範圍最多可包含 10 項存取權範圍規則。每個短期憑證只能套用一個憑證存取邊界。

以 JSON 物件表示時,憑證存取邊界包含下列欄位:

欄位
accessBoundary

object

憑證存取權範圍的包裝函式。

accessBoundary.accessBoundaryRules[]

object

要套用至短期憑證的存取權範圍規則清單。

accessBoundary.accessBoundaryRules[].availablePermissions[]

string

這份清單會定義資源可用權限的上限。

每個值都是 IAM 預先定義角色自訂角色的 ID,並以 inRole: 前置字串開頭。例如: inRole:roles/storage.objectViewer。只有這些角色中的權限可用。

accessBoundary.accessBoundaryRules[].availableResource

string

規則適用的 Cloud Storage 值區完整資源名稱。請使用「//storage.googleapis.com/projects/_/buckets/bucket-name」格式。

accessBoundary.accessBoundaryRules[].availabilityCondition

object

(選用步驟) 限制權限僅適用於特定 Cloud Storage 物件的條件。

如果您想為特定物件 (而非 Cloud Storage 值區中的所有物件) 提供權限,請使用這個欄位。

accessBoundary.accessBoundaryRules[].availabilityCondition.expression

string

條件運算式,用於指定可使用權限的 Cloud Storage 物件。

如要瞭解如何在條件運算式中參照特定物件,請參閱resource.name 屬性api.getAttribute("storage.googleapis.com/objectListPrefix")屬性

accessBoundary.accessBoundaryRules[].availabilityCondition.title

string

(選用步驟) 簡短字串,用於識別條件的用途。

accessBoundary.accessBoundaryRules[].availabilityCondition.description

string

(選用步驟) 條件目的的詳細資料。

如需 JSON 格式的範例,請參閱本頁的「憑證存取權界線範例」。

憑證存取權範圍範例

以下各節將針對常見用途,提供憑證存取權界線的範例。當您以 OAuth 2.0 存取權杖換取範圍縮減的權杖時,就會使用憑證存取邊界。

限制值區的權限

以下範例顯示簡單的憑證存取權界線。這項限制適用於 Cloud Storage 值區 example-bucket,並將權限上限設為 Storage 物件檢視者角色 (roles/storage.objectViewer) 包含的權限:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket"
      }
    ]
  }
}

限制多個值區的權限

以下範例顯示的憑證存取邊界包含多個值區的規則:

  • Cloud Storage 值區 example-bucket-1:這個值區只能使用「Storage 物件檢視者」角色 (roles/storage.objectViewer) 的權限。
  • Cloud Storage 值區 example-bucket-2:這個值區只能使用「Storage 物件建立者」角色 (roles/storage.objectCreator) 的權限。
{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket-1"
      },
      {
        "availablePermissions": [
          "inRole:roles/storage.objectCreator"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket-2"
      }
    ]
  }
}

限制特定物件的權限

您也可以使用 IAM 條件,指定主體可存取的 Cloud Storage 物件。舉例來說,您可以新增條件,讓名稱開頭為 customer-a 的物件可使用權限:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket",
        "availabilityCondition": {
          "expression" : "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
        }
      }
    ]
  }
}

列出物件時限制權限

列出 Cloud Storage 值區中的物件時,您是在呼叫值區資源的方法,而非物件資源。因此,如果系統評估清單要求的條件,且條件參照資源名稱,則資源名稱會識別值區,而非值區內的物件。舉例來說,當您列出 example-bucket 中的物件時,資源名稱為 projects/_/buckets/example-bucket

如果您列出物件,這個命名慣例可能會導致非預期的行為。舉例來說,假設您想要建立憑證存取權界線,允許檢視存取 example-bucket 中前置字串為 customer-a/invoices/ 的物件。您可以在憑證存取權範圍中嘗試使用下列條件:

不完整:只檢查資源名稱的條件

resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/')

這項條件適用於讀取物件,但不適用於列出物件:

  • 如果主體嘗試讀取 example-bucket 中含有 customer-a/invoices/ 前置字元的物件,條件會評估為 true
  • 當主體嘗試列出具有該前置字串的物件時,條件會評估為 falseresource.name 的值為 projects/_/buckets/example-bucket,開頭不是 projects/_/buckets/example-bucket/objects/customer-a/invoices/

為避免發生這個問題,除了使用 resource.name.startsWith() 之外,條件也可以檢查名為 storage.googleapis.com/objectListPrefixAPI 屬性。這個屬性包含用於篩選物件清單的 prefix 參數值。因此,您可以編寫參照 prefix 參數值的條件。

以下範例說明如何在條件中使用 API 屬性。可讀取列出 example-bucket 中具有前置字串 customer-a/invoices/ 的物件:

完成:檢查資源名稱和前置字串的條件

resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/')  ||
    api.getAttribute('storage.googleapis.com/objectListPrefix', '')
                     .startsWith('customer-a/invoices/')

您現在可以在憑證存取權範圍中使用這項條件:

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:roles/storage.objectViewer"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/example-bucket",
        "availabilityCondition": {
          "expression":
            "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a/invoices/') || api.getAttribute('storage.googleapis.com/objectListPrefix', '').startsWith('customer-a/invoices/')"
        }
      }
    ]
  }
}

事前準備

使用憑證存取邊界前,請確認符合下列條件:

  • 您只需要縮減 Cloud Storage 的權限,其他Google Cloud 服務則不需要。

    如要縮減其他 Google Cloud服務的權限範圍,可以建立多個服務帳戶,並為每個服務帳戶授予不同的角色組合。

  • 您可以使用 OAuth 2.0 存取權杖進行驗證。 其他類型的短期憑證不支援憑證存取權界限。

此外,您必須啟用必要的 API:

  • Enable the IAM and Security Token Service APIs.

    Enable the APIs

建立權限範圍縮小的短期憑證

如要建立權限範圍縮減的 OAuth 2.0 存取權杖,請按照下列步驟操作:

  1. 將適當的 IAM 角色授予使用者或服務帳戶。
  2. 定義憑證存取權界線,為使用者或服務帳戶可用的權限設定上限。
  3. 為使用者或服務帳戶建立 OAuth 2.0 存取權杖
  4. 將 OAuth 2.0 存取權杖換成新權杖,新權杖會遵守憑證存取邊界。

接著,您可以使用新的 OAuth 2.0 存取憑證 (範圍縮減) 驗證對 Cloud Storage 的要求。

授予 IAM 角色

憑證存取邊界會為資源的可用權限設定上限。它可以從主體減去權限,但無法新增主體沒有的權限。

因此,您也必須在 Cloud Storage bucket較高層級的資源 (例如專案) 上,授予主體所需的權限。

舉例來說,假設您需要建立範圍縮減的短期憑證,允許服務帳戶在 bucket 中建立物件:

  • 您至少必須授予服務帳戶包含 storage.objects.create 權限的角色,例如儲存空間物件建立者角色 (roles/storage.objectCreator)。憑證存取邊界也必須包含這項權限。
  • 您也可以授予包含更多權限的角色,例如「儲存空間物件管理員」角色 (roles/storage.objectAdmin)。服務帳戶只能使用角色授權和憑證存取權界線中同時顯示的權限。

如要瞭解 Cloud Storage 的預先定義角色,請參閱 Cloud Storage 角色

建立 OAuth 2.0 存取權杖

建立範圍縮減的短期憑證前,您必須先建立一般的 OAuth 2.0 存取權杖。然後,您可以使用一般憑證換取範圍縮減的憑證。建立存取權杖時,請使用 OAuth 2.0 範圍 https://www.googleapis.com/auth/cloud-platform

如要為服務帳戶建立存取權杖,您可以完成伺服器對伺服器的 OAuth 2.0 流程,也可以使用 Service Account Credentials API 產生 OAuth 2.0 存取權杖

如要為使用者建立存取權杖,請參閱「取得 OAuth 2.0 存取權杖」。您也可以使用 OAuth 2.0 Playground,為自己的 Google 帳戶建立存取權杖。

交換 OAuth 2.0 存取權杖

建立 OAuth 2.0 存取憑證後,您可以將存取憑證換成受憑證存取範圍限制的權杖。這個程序通常會涉及權杖代理人權杖消費者

  • 權杖代理人負責定義憑證存取權範圍,並將存取權杖換成權限範圍縮小的權杖。

    權杖代理程式可以使用支援的驗證程式庫自動交換存取權杖,也可以呼叫安全權杖服務手動交換權杖。

  • 權杖消費者會向權杖代理人要求範圍縮減的存取權杖,然後使用該權杖執行其他動作。

    權杖消費者可以使用支援的驗證程式庫,在存取權杖過期前自動更新。或者,也可以手動更新權杖,或允許權杖過期而不更新。

您可以透過下列兩種方式,將存取權杖換成範圍縮減的權杖:

  • 用戶端權杖交換 (建議):用戶端會從 Security Token Service API 伺服器取得加密編譯資料。用戶端可透過加密素材,在用戶端獨立產生具有不同憑證存取邊界規則的範圍縮減權杖,效期為一段時間 (例如 1 小時)。這種做法可縮短延遲時間並提高效率,特別是需要頻繁更新憑證存取邊界規則的用戶端。如果應用程式需要產生許多不重複的範圍縮減權杖,這種做法也更有效率。建議採用這種做法,因為可提供更優異的效能、擴充性,以及日後的功能相容性。

  • 伺服器端權杖交換:每當憑證存取範圍規則變更時,用戶端都會向 Security Token Service API 伺服器要求新的範圍縮減權杖。這種方法很簡單,但每次要求範圍縮減的權杖時,都必須往返於 Security Token Service API 伺服器。由於每個範圍縮減的權杖要求都會往返於安全權杖服務 API,因此建議只有在用戶端程式庫不支援用戶端權杖交換時,才採用這種方法。

用戶端權杖交換

如果您使用下列語言建立權杖中介服務和權杖消費者,即可透過用戶端方法,使用 Google 的驗證程式庫自動交換及更新權杖。

Java

如果是 Java,您可以使用 1.32.1 以上版本的 com.google.auth:google-auth-library-cab-token-generator 構件,自動交換及重新整理權杖。

如要查看您使用的構件版本,請在應用程式目錄中執行下列 Maven 指令:

mvn dependency:list -DincludeArtifactIds=google-auth-library-cab-token-generator

以下範例說明權杖代理程式如何產生範圍縮減的權杖:

import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.CredentialAccessBoundary;
import com.google.auth.oauth2.GoogleCredentials;
import dev.cel.common.CelValidationException;
import java.io.IOException;
import java.security.GeneralSecurityException;
  public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
      throws IOException {
    // Retrieve the source credentials from ADC.
    GoogleCredentials sourceCredentials =
        GoogleCredentials.getApplicationDefault()
            .createScoped("https://www.googleapis.com/auth/cloud-platform");

    // Initialize the Credential Access Boundary rules.
    String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;

    // Downscoped credentials will have readonly access to the resource.
    String availablePermission = "inRole:roles/storage.objectViewer";

    // Only objects starting with the specified prefix string in the object name will be allowed
    // read access.
    String expression =
        "resource.name.startsWith('projects/_/buckets/"
            + bucketName
            + "/objects/"
            + objectPrefix
            + "')";

    // Build the AvailabilityCondition.
    CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
        CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
            .setExpression(expression)
            .build();

    // Define the single access boundary rule using the above properties.
    CredentialAccessBoundary.AccessBoundaryRule rule =
        CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
            .setAvailableResource(availableResource)
            .addAvailablePermission(availablePermission)
            .setAvailabilityCondition(availabilityCondition)
            .build();

    // Define the Credential Access Boundary with all the relevant rules.
    CredentialAccessBoundary credentialAccessBoundary =
        CredentialAccessBoundary.newBuilder().addRule(rule).build();

    // Create an instance of ClientSideCredentialAccessBoundaryFactory.
    ClientSideCredentialAccessBoundaryFactory factory =
        ClientSideCredentialAccessBoundaryFactory.newBuilder()
            .setSourceCredential(sourceCredentials)
            .build();

    // Generate the token and pass it to the Token Consumer.
    try {
      return factory.generateToken(credentialAccessBoundary);
    } catch (GeneralSecurityException | CelValidationException e) {
      throw new IOException("Error generating downscoped token", e);
    }
  }

以下範例說明權杖消費者如何使用重新整理處理常式,自動取得及重新整理範圍縮減的權杖:

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.io.IOException;
  public static String retrieveBlobWithDownscopedToken(
      final String bucketName, final String objectName) throws IOException {
    // You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
    // library to seamlessly handle downscoped token refreshes on expiration.
    OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
        new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
          @Override
          public AccessToken refreshAccessToken() throws IOException {
            // The common pattern of usage is to have a token broker pass the downscoped short-lived
            // access tokens to a token consumer via some secure authenticated channel.
            // For illustration purposes, we are generating the downscoped token locally.
            // We want to test the ability to limit access to objects with a certain prefix string
            // in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
            // not required if access to all bucket resources are allowed. If access to limited
            // resources in the bucket is needed, this mechanism can be used.
            return DownscopedAccessTokenGenerator
                .getTokenFromBroker(bucketName, objectName);
          }
        };

    AccessToken downscopedToken = handler.refreshAccessToken();

    OAuth2CredentialsWithRefresh credentials =
        OAuth2CredentialsWithRefresh.newBuilder()
            .setAccessToken(downscopedToken)
            .setRefreshHandler(handler)
            .build();

    StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
    Storage storage = options.getService();

    Blob blob = storage.get(bucketName, objectName);
    if (blob == null) {
      return null;
    }
    return new String(blob.getContent());
  }

伺服器端權杖交換

以下章節說明如何透過服務端方法交換權杖。

使用伺服器端方法自動交換及更新存取權杖

如果您使用下列其中一種語言建立權杖代理人和權杖消費者,可以透過伺服器端權杖產生方法,使用 Google 的驗證程式庫自動交換及更新權杖:

Go

如果是 Go,您可以使用 golang.org/x/oauth2 套件的 v0.0.0-20210819190943-2bc19b11175f 以上版本,自動交換及重新整理權杖。

如要查看您使用的套件版本,請在應用程式目錄中執行下列指令:

go list -m golang.org/x/oauth2

以下範例說明權杖代理程式如何產生範圍縮減的權杖:


import (
	"context"
	"fmt"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"
)

// createDownscopedToken would be run on the token broker in order to generate
// a downscoped access token that only grants access to objects whose name begins with prefix.
// The token broker would then pass the newly created token to the requesting token consumer for use.
func createDownscopedToken(bucketName string, prefix string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()
	// A condition can optionally be provided to further restrict access permissions.
	condition := downscope.AvailabilityCondition{
		Expression:  "resource.name.startsWith('projects/_/buckets/" + bucketName + "/objects/" + prefix + "')",
		Title:       prefix + " Only",
		Description: "Restricts a token to only be able to access objects that start with `" + prefix + "`",
	}
	// Initializes an accessBoundary with one Rule which restricts the downscoped
	// token to only be able to access the bucket "bucketName" and only grants it the
	// permission "storage.objectViewer".
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
			Condition:            &condition, // Optional
		},
	}

	// This Source can be initialized in multiple ways; the following example uses
	// Application Default Credentials.
	var rootSource oauth2.TokenSource

	// You must provide the "https://www.googleapis.com/auth/cloud-platform" scope.
	rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err != nil {
		return fmt.Errorf("failed to generate rootSource: %w", err)
	}

	// downscope.NewTokenSource constructs the token source with the configuration provided.
	dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
	if err != nil {
		return fmt.Errorf("failed to generate downscoped token source: %w", err)
	}
	// Token() uses the previously declared TokenSource to generate a downscoped token.
	tok, err := dts.Token()
	if err != nil {
		return fmt.Errorf("failed to generate token: %w", err)
	}
	// Pass this token back to the token consumer.
	_ = tok
	return nil
}

以下範例說明權杖消費者如何使用重新整理處理常式,自動取得及重新整理範圍縮減的權杖:


import (
	"context"
	"fmt"
	"io"

	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"

	"cloud.google.com/go/storage"
	"golang.org/x/oauth2"
	"google.golang.org/api/option"
)

// A token consumer should define their own tokenSource. In the Token() method,
// it should send a query to a token broker requesting a downscoped token.
// The token broker holds the root credential that is used to generate the
// downscoped token.
type localTokenSource struct {
	ctx        context.Context
	bucketName string
	brokerURL  string
}

func (lts localTokenSource) Token() (*oauth2.Token, error) {
	var remoteToken *oauth2.Token
	// Usually you would now retrieve remoteToken, an oauth2.Token, from token broker.
	// This snippet performs the same functionality locally.
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + lts.bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
		},
	}
	rootSource, err := google.DefaultTokenSource(lts.ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err != nil {
		return nil, fmt.Errorf("failed to generate rootSource: %w", err)
	}
	dts, err := downscope.NewTokenSource(lts.ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
	if err != nil {
		return nil, fmt.Errorf("failed to generate downscoped token source: %w", err)
	}
	// Token() uses the previously declared TokenSource to generate a downscoped token.
	remoteToken, err = dts.Token()
	if err != nil {
		return nil, fmt.Errorf("failed to generate token: %w", err)
	}

	return remoteToken, nil
}

// getObjectContents will read the contents of an object in Google Storage
// named objectName, contained in the bucket "bucketName".
func getObjectContents(output io.Writer, bucketName string, objectName string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()

	thisTokenSource := localTokenSource{
		ctx:        ctx,
		bucketName: bucketName,
		brokerURL:  "yourURL.com/internal/broker",
	}

	// Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing.
	refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource)
	// You can now use the token source to access Google Cloud Storage resources as follows.
	storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS))
	if err != nil {
		return fmt.Errorf("failed to create the storage client: %w", err)
	}
	defer storageClient.Close()
	bkt := storageClient.Bucket(bucketName)
	obj := bkt.Object(objectName)
	rc, err := obj.NewReader(ctx)
	if err != nil {
		return fmt.Errorf("failed to retrieve the object: %w", err)
	}
	defer rc.Close()
	data, err := io.ReadAll(rc)
	if err != nil {
		return fmt.Errorf("could not read the object's contents: %w", err)
	}
	// Data now contains the contents of the requested object.
	output.Write(data)
	return nil
}

Java

如果是 Java,您可以使用 1.1.0 以上版本的 com.google.auth:google-auth-library-oauth2-http 構件,自動交換及重新整理權杖。

如要查看您使用的構件版本,請在應用程式目錄中執行下列 Maven 指令:

mvn dependency:list -DincludeArtifactIds=google-auth-library-oauth2-http

以下範例說明權杖代理程式如何產生範圍縮減的權杖:

public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
    throws IOException {
  // Retrieve the source credentials from ADC.
  GoogleCredentials sourceCredentials =
      GoogleCredentials.getApplicationDefault()
          .createScoped("https://www.googleapis.com/auth/cloud-platform");

  // Initialize the Credential Access Boundary rules.
  String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;

  // Downscoped credentials will have readonly access to the resource.
  String availablePermission = "inRole:roles/storage.objectViewer";

  // Only objects starting with the specified prefix string in the object name will be allowed
  // read access.
  String expression =
      "resource.name.startsWith('projects/_/buckets/"
          + bucketName
          + "/objects/"
          + objectPrefix
          + "')";

  // Build the AvailabilityCondition.
  CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
      CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
          .setExpression(expression)
          .build();

  // Define the single access boundary rule using the above properties.
  CredentialAccessBoundary.AccessBoundaryRule rule =
      CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
          .setAvailableResource(availableResource)
          .addAvailablePermission(availablePermission)
          .setAvailabilityCondition(availabilityCondition)
          .build();

  // Define the Credential Access Boundary with all the relevant rules.
  CredentialAccessBoundary credentialAccessBoundary =
      CredentialAccessBoundary.newBuilder().addRule(rule).build();

  // Create the downscoped credentials.
  DownscopedCredentials downscopedCredentials =
      DownscopedCredentials.newBuilder()
          .setSourceCredential(sourceCredentials)
          .setCredentialAccessBoundary(credentialAccessBoundary)
          .build();

  // Retrieve the token.
  // This will need to be passed to the Token Consumer.
  AccessToken accessToken = downscopedCredentials.refreshAccessToken();
  return accessToken;
}

以下範例說明權杖消費者如何使用重新整理處理常式,自動取得及重新整理範圍縮減的權杖:

public static void tokenConsumer(final String bucketName, final String objectName)
    throws IOException {
  // You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
  // library to seamlessly handle downscoped token refreshes on expiration.
  OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
      new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
        @Override
        public AccessToken refreshAccessToken() throws IOException {
          // The common pattern of usage is to have a token broker pass the downscoped short-lived
          // access tokens to a token consumer via some secure authenticated channel.
          // For illustration purposes, we are generating the downscoped token locally.
          // We want to test the ability to limit access to objects with a certain prefix string
          // in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
          // not required if access to all bucket resources are allowed. If access to limited
          // resources in the bucket is needed, this mechanism can be used.
          return getTokenFromBroker(bucketName, objectName.substring(0, 3));
        }
      };

  // Downscoped token retrieved from token broker.
  AccessToken downscopedToken = handler.refreshAccessToken();

  // Create the OAuth2CredentialsWithRefresh from the downscoped token and pass a refresh handler
  // which will handle token expiration.
  // This will allow the consumer to seamlessly obtain new downscoped tokens on demand every time
  // token expires.
  OAuth2CredentialsWithRefresh credentials =
      OAuth2CredentialsWithRefresh.newBuilder()
          .setAccessToken(downscopedToken)
          .setRefreshHandler(handler)
          .build();

  // Use the credentials with the Cloud Storage SDK.
  StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
  Storage storage = options.getService();

  // Call Cloud Storage APIs.
  Blob blob = storage.get(bucketName, objectName);
  String content = new String(blob.getContent());
  System.out.println(
      "Retrieved object, "
          + objectName
          + ", from bucket,"
          + bucketName
          + ", with content: "
          + content);
}

Node.js

如果是 Node.js,您可以使用 google-auth-library 套件 7.9.0 以上版本,自動交換及重新整理權杖。

如要查看您使用的套件版本,請在應用程式目錄中執行下列指令:

npm list google-auth-library

以下範例說明權杖代理程式如何產生範圍縮減的權杖:

// Imports the Google Auth libraries.
const {GoogleAuth, DownscopedClient} = require('google-auth-library');
/**
 * Simulates token broker generating downscoped tokens for specified bucket.
 *
 * @param bucketName The name of the Cloud Storage bucket.
 * @param objectPrefix The prefix string of the object name. This is used
 *        to ensure access is restricted to only objects starting with this
 *        prefix string.
 */
async function getTokenFromBroker(bucketName, objectPrefix) {
  const googleAuth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/cloud-platform',
  });

  // Define the Credential Access Boundary object.
  const cab = {
    // Define the access boundary.
    accessBoundary: {
      // Define the single access boundary rule.
      accessBoundaryRules: [
        {
          availableResource: `//storage.googleapis.com/projects/_/buckets/${bucketName}`,
          // Downscoped credentials will have readonly access to the resource.
          availablePermissions: ['inRole:roles/storage.objectViewer'],
          // Only objects starting with the specified prefix string in the object name
          // will be allowed read access.
          availabilityCondition: {
            expression:
              "resource.name.startsWith('projects/_/buckets/" +
              `${bucketName}/objects/${objectPrefix}')`,
          },
        },
      ],
    },
  };

  // Obtain an authenticated client via ADC.
  const client = await googleAuth.getClient();

  // Use the client to create a DownscopedClient.
  const cabClient = new DownscopedClient(client, cab);

  // Refresh the tokens.
  const refreshedAccessToken = await cabClient.getAccessToken();

  // This will need to be passed to the token consumer.
  return refreshedAccessToken;
}

以下範例說明權杖消費者如何提供重新整理處理常式,自動取得及重新整理範圍縮減的權杖:

// Imports the Google Auth and Google Cloud libraries.
const {OAuth2Client} = require('google-auth-library');
const {Storage} = require('@google-cloud/storage');
/**
 * Simulates token consumer generating calling GCS APIs using generated
 * downscoped tokens for specified bucket.
 *
 * @param bucketName The name of the Cloud Storage bucket.
 * @param objectName The name of the object in the Cloud Storage bucket
 *        to read.
 */
async function tokenConsumer(bucketName, objectName) {
  // Create the OAuth credentials (the consumer).
  const oauth2Client = new OAuth2Client();
  // We are defining a refresh handler instead of a one-time access
  // token/expiry pair.
  // This will allow the consumer to obtain new downscoped tokens on
  // demand every time a token is expired, without any additional code
  // changes.
  oauth2Client.refreshHandler = async () => {
    // The common pattern of usage is to have a token broker pass the
    // downscoped short-lived access tokens to a token consumer via some
    // secure authenticated channel. For illustration purposes, we are
    // generating the downscoped token locally. We want to test the ability
    // to limit access to objects with a certain prefix string in the
    // resource bucket. objectName.substring(0, 3) is the prefix here. This
    // field is not required if access to all bucket resources are allowed.
    // If access to limited resources in the bucket is needed, this mechanism
    // can be used.
    const refreshedAccessToken = await getTokenFromBroker(
      bucketName,
      objectName.substring(0, 3)
    );
    return {
      access_token: refreshedAccessToken.token,
      expiry_date: refreshedAccessToken.expirationTime,
    };
  };

  const storageOptions = {
    projectId: process.env.GOOGLE_CLOUD_PROJECT,
    authClient: oauth2Client,
  };

  const storage = new Storage(storageOptions);
  const downloadFile = await storage
    .bucket(bucketName)
    .file(objectName)
    .download();
  console.log(downloadFile.toString('utf8'));
}

Python

如果是 Python,您可以使用 google-auth 套件 2.0.0 以上版本,自動交換及重新整理權杖。

如要查看您使用的套件版本,請在安裝套件的環境中執行下列指令:

pip show google-auth

以下範例說明權杖代理程式如何產生範圍縮減的權杖:

import google.auth

from google.auth import downscoped
from google.auth.transport import requests

def get_token_from_broker(bucket_name, object_prefix):
    """Simulates token broker generating downscoped tokens for specified bucket.

    Args:
        bucket_name (str): The name of the Cloud Storage bucket.
        object_prefix (str): The prefix string of the object name. This is used
            to ensure access is restricted to only objects starting with this
            prefix string.

    Returns:
        Tuple[str, datetime.datetime]: The downscoped access token and its expiry date.
    """
    # Initialize the Credential Access Boundary rules.
    available_resource = f"//storage.googleapis.com/projects/_/buckets/{bucket_name}"
    # Downscoped credentials will have readonly access to the resource.
    available_permissions = ["inRole:roles/storage.objectViewer"]
    # Only objects starting with the specified prefix string in the object name
    # will be allowed read access.
    availability_expression = (
        "resource.name.startsWith('projects/_/buckets/{}/objects/{}')".format(
            bucket_name, object_prefix
        )
    )
    availability_condition = downscoped.AvailabilityCondition(availability_expression)
    # Define the single access boundary rule using the above properties.
    rule = downscoped.AccessBoundaryRule(
        available_resource=available_resource,
        available_permissions=available_permissions,
        availability_condition=availability_condition,
    )
    # Define the Credential Access Boundary with all the relevant rules.
    credential_access_boundary = downscoped.CredentialAccessBoundary(rules=[rule])

    # Retrieve the source credentials via ADC.
    source_credentials, _ = google.auth.default()
    if source_credentials.requires_scopes:
        source_credentials = source_credentials.with_scopes(
            ["https://www.googleapis.com/auth/cloud-platform"]
        )

    # Create the downscoped credentials.
    downscoped_credentials = downscoped.Credentials(
        source_credentials=source_credentials,
        credential_access_boundary=credential_access_boundary,
    )

    # Refresh the tokens.
    downscoped_credentials.refresh(requests.Request())

    # These values will need to be passed to the token consumer.
    access_token = downscoped_credentials.token
    expiry = downscoped_credentials.expiry
    return (access_token, expiry)

以下範例說明權杖消費者如何提供重新整理處理常式,自動取得及重新整理範圍縮減的權杖:

from google.cloud import storage
from google.oauth2 import credentials

def token_consumer(bucket_name, object_name):
    """Tests token consumer readonly access to the specified object.

    Args:
        bucket_name (str): The name of the Cloud Storage bucket.
        object_name (str): The name of the object in the Cloud Storage bucket
            to read.
    """

    # Create the OAuth credentials from the downscoped token and pass a
    # refresh handler to handle token expiration. We are passing a
    # refresh_handler instead of a one-time access token/expiry pair.
    # This will allow the consumer to obtain new downscoped tokens on
    # demand every time a token is expired, without any additional code
    # changes.
    def refresh_handler(request, scopes=None):
        # The common pattern of usage is to have a token broker pass the
        # downscoped short-lived access tokens to a token consumer via some
        # secure authenticated channel.
        # For illustration purposes, we are generating the downscoped token
        # locally.
        # We want to test the ability to limit access to objects with a certain
        # prefix string in the resource bucket. object_name[0:3] is the prefix
        # here. This field is not required if access to all bucket resources are
        # allowed. If access to limited resources in the bucket is needed, this
        # mechanism can be used.
        return get_token_from_broker(bucket_name, object_prefix=object_name[0:3])

    creds = credentials.Credentials(
        None,
        scopes=["https://www.googleapis.com/auth/cloud-platform"],
        refresh_handler=refresh_handler,
    )

    # Initialize a Cloud Storage client with the oauth2 credentials.
    storage_client = storage.Client(credentials=creds)
    # The token broker has readonly access to the specified bucket object.
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(object_name)
    print(blob.download_as_bytes().decode("utf-8"))

手動交換及重新整理存取權杖

權杖代理商可以使用 Security Token Service API,將存取權杖換成範圍縮減的存取權杖。然後將範圍縮減的權杖提供給權杖消費者。

如要交換存取權杖,請使用下列 HTTP 方法和網址:

POST https://sts.googleapis.com/v1/token

將要求中的 Content-Type 標頭設為 application/x-www-form-urlencoded。在要求主體中加入下列欄位:

欄位
grant_type

string

請使用 urn:ietf:params:oauth:grant-type:token-exchange 值。

options

string

採用 JSON 格式的憑證存取邊界,並以百分比編碼編碼。

requested_token_type

string

請使用 urn:ietf:params:oauth:token-type:access_token 值。

subject_token

string

要交換的 OAuth 2.0 存取權杖。

subject_token_type

string

請使用 urn:ietf:params:oauth:token-type:access_token 值。

回應為 JSON 物件,內含下列欄位:

欄位
access_token

string

遵守憑證存取權範圍的範圍縮減 OAuth 2.0 存取權杖。

expires_in

number

縮減範圍的權杖失效前的時間長度 (以秒為單位)。

只有在原始存取權杖代表服務帳戶時,才會顯示這個欄位。如果沒有這個欄位,縮減範圍的權杖到期時間會與原始存取權杖相同。

issued_token_type

string

包含 urn:ietf:params:oauth:token-type:access_token 值。

token_type

string

包含值 Bearer

舉例來說,如果 JSON 格式的憑證存取權界線儲存在 ./access-boundary.json 檔案中,您可以使用下列 curl 指令交換存取權杖。將 original-token 替換為原始存取權杖:

curl -H "Content-Type:application/x-www-form-urlencoded" \
    -X POST \
    https://sts.googleapis.com/v1/token \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=original-token" \
    --data-urlencode "options=$(cat ./access-boundary.json)"

回應類似下列範例:

{
  "access_token": "ya29.dr.AbCDeFg-123456...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}

當權杖消費者要求縮減範圍的權杖時,權杖代理程式應同時回應縮減範圍的權杖,以及權杖到期前的秒數。如要重新整理範圍縮減的權杖,消費者可以在現有權杖到期前,向代理人要求範圍縮減的權杖。

後續步驟