From eb57311a00818ed2053b762606d5254d305371fc Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 10 Jun 2021 14:11:50 -0700 Subject: [PATCH] Add some validity testing for AccessBoundaryRules and add documentation. --- .../{downscoped => downscope}/downscoping.go | 64 ++++++++++++++++--- .../downscoping_test.go | 2 +- 2 files changed, 56 insertions(+), 10 deletions(-) rename google/{downscoped => downscope}/downscoping.go (50%) rename google/{downscoped => downscope}/downscoping_test.go (99%) diff --git a/google/downscoped/downscoping.go b/google/downscope/downscoping.go similarity index 50% rename from google/downscoped/downscoping.go rename to google/downscope/downscoping.go index 80677d4..2b9d113 100644 --- a/google/downscoped/downscoping.go +++ b/google/downscope/downscoping.go @@ -1,4 +1,9 @@ -package downscoped +/* +Package downscope implements the ability to downwcope, or restrict, the +Identity and AccessManagement permissions that a short-lived Token +can use. Please note that only Google Cloud Storage supports this feature. + */ +package downscope import ( "context" @@ -16,19 +21,40 @@ const ( // Defines an upper bound of permissions available for a GCP credential for one or more resources type AccessBoundary struct { - // One or more AccessBoundaryRules are required to define permissions for the new downscoped token + // One or more AccessBoundaryRules are required to define permissions + // for the new downscoped token. Each one defines an access (or set of accesses) + // that the new token has to a given resource. AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` } +// An AvailabilityCondition restricts access to a given Resource. type AvailabilityCondition struct { - Title string `json:"title,omitempty"` + // A condition expression that specifies the Cloud Storage objects where + // permissions are available. For further documentation, see + // https://cloud.google.com/iam/docs/conditions-overview Expression string `json:"expression"` + // Optional. A short string that identifies the purpose of the condition. + Title string `json:"title,omitempty"` + // Optional. Details about the purpose of the condition. Description string `json:"description,omitempty"` } +// Sets the permissions (and optionally conditions) that the new +// token has on given resource. type AccessBoundaryRule struct { + // AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to. + // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. AvailableResource string `json:"availableResource"` + // AvailablePermissions is a list that defines the upper bound on the available permissions + // for the resource. Each value is the identifier for an IAM predefined role or custom role, + // with the prefix inRole:. For example: inRole:roles/storage.objectViewer. + // Only the permissions in these roles will be available. AvailablePermissions []string `json:"availablePermissions"` + // An optional Condition that restricts the availability of permissions + // to specific Cloud Storage objects. + // + // Use this field if you want to make permissions available for specific objects, + // rather than all objects in a Cloud Storage bucket. Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` } @@ -39,17 +65,33 @@ type downscopedTokenResponse struct { ExpiresIn int `json:"expires_in"` } +// Specifies the information necessary to request a downscoped token. type DownscopingConfig struct { + // RootSource is the TokenSource used to create the downscoped token. + // The downscoped token therefore has some subset of the accesses of + // the original RootSource. RootSource oauth2.TokenSource + // CredentialAccessBoundary defines the accesses held by the new + // downscoped Token. CredentialAccessBoundary AccessBoundary } +// downscopedTokenWithEndpoint is a helper function used for unit testing +// purposes, as it allows us to pass in a locally mocked endpoint. func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, endpoint string) (oauth2.TokenSource, error) { if config.RootSource == nil { - return nil, fmt.Errorf("oauth2/google/downscoped: rootTokenSource cannot be nil") + return nil, fmt.Errorf("downscope: rootTokenSource cannot be nil") } if len(config.CredentialAccessBoundary.AccessBoundaryRules) == 0 { - return nil, fmt.Errorf("oauth2/google/downscoped: length of AccessBoundaryRules must be at least 1") + return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") + } + for _, val := range config.CredentialAccessBoundary.AccessBoundaryRules { + if val.AvailableResource == "" { + return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) + } + if len(val.AvailablePermissions) == 0 { + return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val) + } } downscopedOptions := struct { @@ -60,12 +102,12 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, tok, err := config.RootSource.Token() if err != nil { - return nil, fmt.Errorf("oauth2/google/downscoped: unable to refresh root token %v", err) + return nil, fmt.Errorf("downscope: unable to obtain root token: %v", err) } b, err := json.Marshal(downscopedOptions) if err != nil { - return nil, fmt.Errorf("oauth2/google/downscoped: Unable to marshall AccessBoundary payload %v", err) + return nil, fmt.Errorf("downscope: Unable to marshall AccessBoundary payload %v", err) } form := url.Values{} @@ -88,7 +130,7 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, return nil, fmt.Errorf("unable to unmarshal response body: %v", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unable to exchange token %v", tresp) + return nil, fmt.Errorf("unable to exchange token; %v", resp.StatusCode) } // an exchanged token that is derived from a service account (2LO) has an expired_in value @@ -110,6 +152,10 @@ func downscopedTokenWithEndpoint(ctx context.Context, config DownscopingConfig, return oauth2.StaticTokenSource(newToken), nil } -func NewDownscopedTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { +// NewTokenSource takes a root TokenSource and returns a downscoped TokenSource +// with a subset of the permissions held by the root source. The +// CredentialAccessBoundary in the config defines the permissions held +// by the new TokenSource. +func NewTokenSource(ctx context.Context, config DownscopingConfig) (oauth2.TokenSource, error) { return downscopedTokenWithEndpoint(ctx, config, identityBindingEndpoint) } diff --git a/google/downscoped/downscoping_test.go b/google/downscope/downscoping_test.go similarity index 99% rename from google/downscoped/downscoping_test.go rename to google/downscope/downscoping_test.go index d25d2e0..ee75632 100644 --- a/google/downscoped/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -1,4 +1,4 @@ -package downscoped +package downscope import ( "context"