Réduire les champs d'application avec des limites d'accès aux identifiants

Cette page explique comment utiliser les limites d'accès aux identifiants pour réduire ou restreindre les autorisations IAM (Identity and Access Management) qu'un identifiant de courte durée peut utiliser.

Fonctionnement des limites d'accès aux identifiants

Pour réduire le champ d'application des autorisations, vous définissez une limite d'accès aux identifiants qui spécifie les ressources auxquelles l'identifiant éphémère peut accéder, ainsi que la limite supérieure des autorisations disponibles pour chaque ressource. Vous pouvez ensuite créer un identifiant éphémère, puis l'échanger contre un identifiant qui respecte la limite d'accès aux identifiants.

Si vous souhaitez octroyer aux comptes principaux un ensemble distinct d'autorisations pour chaque session, l'utilisation des limites d'accès aux identifiants peut s'avérer plus efficace que la création de nombreux comptes et l'octroi d'un ensemble différent de rôles à chaque compte de service. Par exemple, si l'un de vos clients doit accéder aux données Cloud Storage que vous contrôlez, vous pouvez créer un compte de service pouvant accéder à chaque bucket Cloud Storage que vous possédez, puis appliquer une limite d'accès aux identifiants qui autorise uniquement l'accès au bucket contenant les données de votre client.

Exemples de limites d'accès aux identifiants

Les sections suivantes présentent des exemples de limites d'accès aux identifiants pour les cas d'utilisation courants. Vous utilisez la limite d'accès aux identifiants lorsque vous échangez un jeton d'accès OAuth 2.0 avec un jeton aux champs d'application limités.

Limiter les autorisations pour un bucket

L'exemple suivant illustre une limite d'accès aux identifiants élémentaire. Elle s'applique au bucket Cloud Storage example-bucket et définit la limite supérieure des autorisations incluses dans le rôle Lecteur d'objets Storage (roles/storage.objectViewer) :

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

Limiter les autorisations pour plusieurs buckets

L'exemple suivant illustre une limite d'accès aux identifiants qui inclut des règles pour plusieurs buckets :

  • Le bucket Cloud Storage example-bucket-1 : pour ce bucket, seules les autorisations du rôle "Lecteur des objets Storage" (roles/storage.objectViewer) sont disponibles.
  • Le bucket Cloud Storage example-bucket-2 : pour ce bucket, seules les autorisations du rôle "Créateur d'objets Storage" (roles/storage.objectCreator) sont disponibles.
{
  "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"
      }
    ]
  }
}

Limiter les autorisations pour des objets spécifiques

Vous pouvez également utiliser les conditions IAM pour spécifier les objets Cloud Storage auxquels un compte principal peut accéder. Par exemple, vous pouvez ajouter une condition qui rend disponibles les autorisations pour les objets dont le nom commence par 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')"
        }
      }
    ]
  }
}

Limiter les autorisations lors de la création d'une liste d'objets

Lorsque vous répertoriez les objets dans un bucket Cloud Storage, vous appelez une méthode sur une ressource de bucket, et non sur une ressource d'objet. Par conséquent, si une condition est évaluée pour une requête de liste et que la condition fait référence au nom de ressource, ce nom identifie le bucket et non pas un objet dans le bucket. Par exemple, lorsque vous répertoriez des objets dans example-bucket, le nom de la ressource est projects/_/buckets/example-bucket.

Cette convention de dénomination peut entraîner un comportement inattendu lorsque vous répertoriez des objets. Par exemple, supposons que vous souhaitiez instaurer une limite d'accès aux identifiants qui autorise l'accès en lecture aux objets de example-bucket comportant le préfixe customer-a/invoices/. Vous pouvez essayer d'utiliser la condition suivante dans la limite d'accès aux identifiants :

Incomplete : condition qui vérifie uniquement le nom de la ressource

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

Cette condition fonctionne pour la lecture d'objets, mais pas pour la création d'une liste des objets :

  • Lorsqu'un compte principal tente de lire un objet dans example-bucket comportant le préfixe customer-a/invoices/, la condition renvoie la valeur true.
  • Lorsqu'un compte principal essaie de répertorier des objets comportant ce préfixe, la condition renvoie la valeur false. La valeur de resource.name, à savoir projects/_/buckets/example-bucket, ne commence pas par projects/_/buckets/example-bucket/objects/customer-a/invoices/.

Pour éviter ce type de problème à l'avenir, votre condition peut utiliser resource.name.startsWith() et vérifier un attribut d'API nommé storage.googleapis.com/objectListPrefix. Cet attribut contient la valeur du paramètre prefix utilisé pour filtrer la liste des objets. Par conséquent, vous pouvez écrire une condition faisant référence à la valeur du paramètre prefix.

L'exemple suivant montre comment utiliser l'attribut d'API dans une condition. Cela permet de lire et de répertorier les objets de example-bucket comportant le préfixe customer-a/invoices/ :

Complete : condition qui vérifie le nom de la ressource et le préfixe

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

Vous pouvez désormais utiliser cette condition dans une limite d'accès aux identifiants :

{
  "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/')"
        }
      }
    ]
  }
}

Avant de commencer

Avant d'utiliser les limites d'accès aux identifiants, assurez-vous de remplir les conditions suivantes :

  • Vous devez réduire le champ d'application des autorisations pour Cloud Storage uniquement, et non pour les autres services Google Cloud.

    Si vous devez réduire le champ d'application des autorisations associées à d'autres services Google Cloud, vous pouvez créer plusieurs comptes de service et attribuer un ensemble de rôles différent à chaque compte de service.

  • Vous utilisez un accès uniforme au niveau du bucket pour gérer l'accès à vos ressources Cloud Storage.

  • Vous pouvez utiliser les jetons d'accès OAuth 2.0 pour l'authentification. Les autres types d'identifiants éphémères ne sont pas compatibles avec les limites d'accès aux identifiants.

En outre, vous devez activer les API nécessaires :

  • Activer les API IAM and Security Token Service.

    Activer les API

Créer un identifiant éphémère à champ d'application limité

Pour créer un jeton d'accès OAuth 2.0 avec des autorisations à champ d'application limité, procédez comme suit :

  1. Attribuez les rôles IAM appropriés à un utilisateur ou à un compte de service.
  2. Définissez une limite d'accès aux identifiants qui définit la limite supérieure des autorisations disponibles pour l'utilisateur ou le compte de service.
  3. Créez un jeton d'accès OAuth 2.0 pour l'utilisateur ou le compte de service.
  4. Échangez le jeton d'accès OAuth 2.0 contre un nouveau jeton respectant la limite d'accès aux identifiants.

Vous pouvez ensuite utiliser le nouveau jeton d'accès OAuth 2.0 à champ d'application limité pour authentifier les requêtes dans Cloud Storage.

Accorder des rôles IAM

Une limite d'accès aux identifiants définit la limite supérieure des autorisations disponibles pour une ressource. Cette limite peut retirer des autorisations à un compte principal, mais ne peut pas ajouter d'autorisations que le compte principal ne possède pas déjà.

Par conséquent, vous devez attribuer au compte principal des rôles qui fournissent les autorisations nécessaires dans un bucket Cloud Storage ou sur une ressource de niveau supérieur, comme le projet.

Supposons, par exemple, que vous ayez besoin de créer des identifiants éphémères à champ d'application limité, permettant à un compte de service de créer des objets dans un bucket :

  • Vous devez au moins octroyer au compte de service un rôle comprenant l'autorisation storage.objects.create, par exemple le rôle Créateur d'objets Storage (roles/storage.objectCreator). La limite d'accès aux identifiants doit également inclure cette autorisation.
  • Vous pouvez également attribuer un rôle comprenant davantage d'autorisations, tel que le rôle Administrateur d'objets Storage (roles/storage.objectAdmin). Le compte de service ne peut utiliser que les autorisations qui apparaissent à la fois dans le rôle attribué et dans la limite d'accès aux identifiants.

Pour en savoir plus sur les rôles prédéfinis pour Cloud Storage, consultez la page Rôles Cloud Storage.

Composants d'une limite d'accès aux identifiants

Une limite d'accès aux identifiants est un objet qui contient une liste de règles de limite d'accès. Chaque règle contient les informations suivantes :

  • La ressource à laquelle s'applique la règle.
  • La limite supérieure des autorisations disponibles pour cette ressource.
  • Facultatif : une condition qui limite davantage les autorisations. Une condition inclut les éléments suivants :
    • Une expression de condition qui renvoie true ou false. Si elle renvoie true, l'accès est autorisé. Sinon, l'accès est refusé.
    • Facultatif : un titre identifiant la condition.
    • Facultatif : une description contenant des informations complémentaires sur la condition.

Si vous appliquez une limite d'accès à un identifiant éphémère, celui-ci ne peut accéder qu'aux ressources spécifiées dans la limite d'accès aux identifiants. Aucune autorisation n'est disponible pour les autres ressources.

Une limite d'accès aux identifiants peut contenir jusqu'à 10 règles de limite d'accès. Vous ne pouvez appliquer qu'une seule limite d'accès par identifiant éphémère.

Lorsqu'elle est représentée sous la forme d'un objet JSON, une limite d'accès aux identifiants contient les champs suivants :

Champs
accessBoundary

object

Wrapper pour la limite d'accès aux identifiants.

accessBoundary.accessBoundaryRules[]

object

Liste des règles de limite d'accès à appliquer à un identifiant éphémère.

accessBoundary.accessBoundaryRules[].availablePermissions[]

string

Liste qui définit la limite supérieure des autorisations disponibles pour la ressource.

Chaque valeur est l'identifiant d'un rôle prédéfini ou d'un rôle personnalisé IAM, avec le préfixe inRole:. Par exemple : inRole:roles/storage.objectViewer. Seules les autorisations associées à ces rôles sont disponibles.

accessBoundary.accessBoundaryRules[].availableResource

string

Nom de ressource complet du bucket Cloud Storage auquel la règle s'applique. Utilisez le format "//storage.googleapis.com/projects/_/buckets/bucket-name".

accessBoundary.accessBoundaryRules[].availabilityCondition

object

Facultatif. Condition limitant la disponibilité des autorisations à des objets Cloud Storage spécifiques.

Utilisez ce champ si vous souhaitez que les autorisations soient disponibles pour des objets spécifiques plutôt que pour tous les objets d'un bucket Cloud Storage.

accessBoundary.accessBoundaryRules[].availabilityCondition.expression

string

Expression de condition qui spécifie les objets Cloud Storage pour lesquels des autorisations sont disponibles.

Pour savoir comment faire référence à des objets spécifiques dans une expression de condition, consultez Attribut resource.name.

accessBoundary.accessBoundaryRules[].availabilityCondition.title

string

Facultatif. Chaîne courte qui identifie la finalité de la condition.

accessBoundary.accessBoundaryRules[].availabilityCondition.description

string

Facultatif. Informations concernant la finalité de la condition.

Pour obtenir des exemples au format JSON, consultez la section Exemples de limites d'accès aux identifiants sur cette page.

Créer un jeton d'accès OAuth 2.0

Avant de créer un identifiant éphémère à champ d'application limité, vous devez créer un jeton d'accès OAuth 2.0 standard. Vous pouvez ensuite échanger l'identifiant standard contre un identifiant à champ d'application limité. Lorsque vous créez le jeton d'accès, utilisez le champ d'application OAuth 2.0 https://www.googleapis.com/auth/cloud-platform.

Pour créer un jeton d'accès pour un compte de service, vous pouvez mettre en œuvre le flux OAuth 2.0 de serveur à serveur ou utiliser l'API Service Account Credentials pour générer un jeton d'accès OAuth 2.0.

Pour créer un jeton d'accès pour un utilisateur, consultez la page Obtenir des jetons d'accès OAuth 2.0. Vous pouvez également utiliser OAuth 2.0 Playground pour créer un jeton d'accès pour votre propre compte Google.

Échanger le jeton d'accès OAuth 2.0

Une fois le jeton d'accès OAuth 2.0 créé, vous pouvez l'échanger contre un nouveau jeton à champ d'application limité respectant la limite d'accès aux identifiants. Ce processus implique généralement un agent de service de jetons et un consommateur de jetons:

  • L'agent de service de jetons est chargé de définir les limites d'accès aux identifiants et d'échanger un jeton d'accès contre un jeton à champ d'application limité.

    L'agent de service de jetons peut utiliser une bibliothèque d'authentification compatible pour échanger automatiquement des jetons d'accès ou appeler le service de jetons de sécurité pour échanger des jetons manuellement.

  • Le consommateur de jetons demande un jeton d'accès à champ d'application limité à l'agent de service de jetons, puis utilise ce jeton pour effectuer une autre action.

    Le consommateur de jetons peut utiliser une bibliothèque d'authentification compatible pour actualiser automatiquement les jetons d'accès avant leur expiration. Il peut également actualiser les jetons manuellement, ou encore autoriser l'expiration des jetons sans les actualiser.

Échanger et actualiser automatiquement le jeton d'accès

Si vous créez l'agent de service de jetons et le consommateur de jetons dans l'un des langages suivants, vous pouvez utiliser la bibliothèque d'authentification de Google pour échanger et actualiser automatiquement les jetons :

Go

Pour Go, vous pouvez échanger et actualiser automatiquement des jetons avec la version v0.0.0-20210819190943-2bc19b11175f ou une version ultérieure du package golang.org/x/oauth2.

Pour vérifier quelle version de ce package vous utilisez, exécutez la commande suivante dans le répertoire de votre application :

go list -m golang.org/x/oauth2

L'exemple suivant montre comment un agent de service de jetons peut générer des jetons à champ d'application limité :


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: %v", 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: %v", 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: %v", err)
	}
	// Pass this token back to the token consumer.
	_ = tok
	return nil
}

L'exemple suivant montre comment un consommateur de jetons peut utiliser un gestionnaire d'actualisation pour obtenir et actualiser automatiquement les jetons à champ d'application limité :


import (
	"context"
	"fmt"
	"io"
	"io/ioutil"

	"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: %v", 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: %v", 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: %v", 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: %v", 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: %v", err)
	}
	defer rc.Close()
	data, err := ioutil.ReadAll(rc)
	if err != nil {
		return fmt.Errorf("could not read the object's contents: %v", err)
	}
	// Data now contains the contents of the requested object.
	output.Write(data)
	return nil
}

Java

Pour Java, vous pouvez échanger et actualiser automatiquement des jetons avec la version 1.1.0 ou une version ultérieure de l'artefact com.google.auth:google-auth-library-oauth2-http.

Pour vérifier quelle version de cet artefact vous utilisez, exécutez la commande Maven suivante dans le répertoire de votre application :

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

L'exemple suivant montre comment un agent de service de jetons peut générer des jetons à champ d'application limité :

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;
}

L'exemple suivant montre comment un consommateur de jetons peut utiliser un gestionnaire d'actualisation pour obtenir et actualiser automatiquement les jetons à champ d'application limité :

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

Pour Node.js, vous pouvez échanger et actualiser automatiquement des jetons avec la version 7.9.0 ou une version ultérieure du package google-auth-library.

Pour vérifier quelle version de ce package vous utilisez, exécutez la commande suivante dans le répertoire de votre application :

npm list google-auth-library

L'exemple suivant montre comment un agent de service de jetons peut générer des jetons à champ d'application limité :

// 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;
}

L'exemple suivant montre comment un consommateur de jetons peut fournir un gestionnaire d'actualisation qui obtient et actualise automatiquement les jetons à champ d'application limité :

// 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

Pour Python, vous pouvez échanger et actualiser automatiquement des jetons avec la version 2.0.0 ou une version ultérieure du package google-auth.

Pour vérifier quelle version de ce package vous utilisez, exécutez la commande suivante dans l'environnement où le package est installé :

pip show google-auth

L'exemple suivant montre comment un agent de service de jetons peut générer des jetons à champ d'application limité :

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)

L'exemple suivant montre comment un consommateur de jetons peut fournir un gestionnaire d'actualisation qui obtient et actualise automatiquement les jetons à champ d'application limité :

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"))

Échanger et actualiser manuellement le jeton d'accès

Un agent de service de jetons peut utiliser l'API Security Token Service pour échanger un jeton d'accès contre un jeton d'accès à champ d'application limité. Il peut ensuite fournir le jeton à champ d'application limité à un consommateur de jetons.

Pour échanger le jeton d'accès, utilisez la méthode HTTP et l'URL suivantes :

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

Définissez l'en-tête Content-Type de la requête sur application/x-www-form-urlencoded. Spécifiez les champs suivants dans le corps de la requête :

Champs
grant_type

string

Utilisez la valeur urn:ietf:params:oauth:grant-type:token-exchange.

options

string

Limite d'accès aux identifiants au format JSON, encodée avec le encodage-pourcent.

requested_token_type

string

Utilisez la valeur urn:ietf:params:oauth:token-type:access_token.

subject_token

string

Jeton d'accès OAuth 2.0 que vous souhaitez échanger.

subject_token_type

string

Utilisez la valeur urn:ietf:params:oauth:token-type:access_token.

La réponse est un objet JSON qui contient les champs suivants :

Champs
access_token

string

Jeton d'accès OAuth 2.0 à champ d'application limité respectant la limite d'accès aux identifiants.

expires_in

number

 : délai avant expiration du nouveau jeton d'accès à champ d'application limité, exprimé en secondes.

Ce champ n'est présent que si le jeton d'accès d'origine représente un compte de service. Lorsque ce champ n'est pas présent, le délai avant expiration du nouveau jeton d'accès à champ d'application limité est le même que celui du jeton d'accès d'origine.

issued_token_type

string

Contient la valeur urn:ietf:params:oauth:token-type:access_token.

token_type

string

Contient la valeur Bearer.

Par exemple, si une limite d'accès aux identifiants au format JSON est stockée dans le fichier ./access-boundary.json, vous pouvez utiliser la commande curl suivante pour échanger le jeton d'accès. Remplacez original-token par le jeton d'accès d'origine :

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)"

La réponse est semblable à l'exemple suivant :

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

Lorsqu'un consommateur de jetons demande un jeton à champ d'application limité, l'agent de service de jetons doit répondre avec le jeton à champ d'application limité et le nombre de secondes avant expiration. Pour actualiser le jeton à champ d'application limité, le consommateur peut demander un jeton à champ d'application limité à l'agent avant l'expiration du jeton existant.

Étape suivante