Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Upbound auth source and support for federated identity #206

Merged
merged 8 commits into from
Feb 6, 2023
Merged
26 changes: 25 additions & 1 deletion apis/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,36 @@ type ProviderConfigSpec struct {
// ProviderCredentials required to authenticate.
type ProviderCredentials struct {
// Source of the provider credentials.
// +kubebuilder:validation:Enum=None;Secret;AccessToken;InjectedIdentity;Environment;Filesystem
// +kubebuilder:validation:Enum=None;Secret;AccessToken;InjectedIdentity;Environment;Filesystem;Upbound
Source xpv1.CredentialsSource `json:"source"`

// Upbound defines the options for authenticating using Upbound as an
// identity provider.
Upbound *Upbound `json:"upbound,omitempty"`

xpv1.CommonCredentialSelectors `json:",inline"`
}

// Upbound defines the options for authenticating using Upbound as an identity
// provider.
type Upbound struct {
// Federation is the configuration for federated identity.
Federation *Federation `json:"federation,omitempty"`
}

// Federation defines the configuration for federated identity from an external
// provider.
type Federation struct {
// ProviderID is the fully-qualified identifier for the identity provider on
// GCP. The format is
// `project/<project-id>/locations/global/workloadIdentityPools/<identity-pool>/providers/<identity-provider>`.
// +kubebuilder:validation:MinLength=1
ProviderID string `json:"providerID"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we add some validation markers for these (at least to make sure they are non-empty)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ulucinar I would love to -- what is y'all's current standard here? Just using validation such as min-length or have y'all started incorporating CEL experessions?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe just a length check would be great for now if you also think it's worth. I don't know of any examples of regex checking or CEL-expressions yet. Unfortunately, we do not have a high standard here yet :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ulucinar sounds reasonable -- thanks for the suggestion and context!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated!

// ServiceAccount is the email address for the attached service account.
// +kubebuilder:validation:MinLength=1
ServiceAccount string `json:"serviceAccount"`
}

// A ProviderConfigStatus reflects the observed state of a ProviderConfig.
type ProviderConfigStatus struct {
xpv1.ProviderConfigStatus `json:",inline"`
Expand Down
40 changes: 40 additions & 0 deletions apis/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/generated.lst

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions examples/providerconfig/upbound.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: gcp.upbound.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
projectID: crossplane-playground
credentials:
source: Upbound
upbound:
federation:
providerID: projects/<project-id>/locations/global/workloadIdentityPools/<identity-pool>/providers/<identity-provider>
serviceAccount: <service-account-name>@<project-name>.iam.gserviceaccount.com
62 changes: 56 additions & 6 deletions internal/clients/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package clients

import (
"context"
"encoding/json"
"fmt"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/resource"
Expand All @@ -32,23 +34,62 @@ import (
const (
keyProject = "project"

credentialsSourceUpbound = "Upbound"
keyCredentials = "credentials"
credentialsSourceAccessToken = "AccessToken"
keyAccessToken = "access_token"

upboundProviderIdentityTokenFile = "/var/run/secrets/upbound.io/provider/token"
)

const (
// error messages
errNoProviderConfig = "no providerConfigRef provided"
errGetProviderConfig = "cannot get referenced ProviderConfig"
errTrackUsage = "cannot track ProviderConfig usage"
errExtractKeyCredentials = "cannot extract JSON key credentials"
errExtractTokenCredentials = "cannot extract Access Token credentials"
errNoProviderConfig = "no providerConfigRef provided"
errGetProviderConfig = "cannot get referenced ProviderConfig"
errTrackUsage = "cannot track ProviderConfig usage"
errExtractKeyCredentials = "cannot extract JSON key credentials"
errExtractTokenCredentials = "cannot extract Access Token credentials"
errConstructFederatedCredentials = "cannot construct federated identity credentials"
errMissingFederatedConfiguration = "missing identity federation configuration"
)

// federatedCredentials is the expected client credential configuration
// structure for federated identity.
type federatedCredentials struct {
Type string `json:"type"`
Audience string `json:"audience"`
SubjectTokenType string `json:"subject_token_type"`
TokenURL string `json:"token_url"`
CredentialSource credentialFileSource `json:"credential_source"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
}

// credentialFileSource is the source of the credential data to be used with
// federated identity.
type credentialFileSource struct {
File string `json:"file"`
}

// constructFederatedCredentials constructs federated identity credentials with
// the provided identity provider and service account.
func constructFederatedCredentials(providerID, serviceAccount string) ([]byte, error) {
return json.Marshal(&federatedCredentials{
Type: "external_account",
Audience: fmt.Sprintf("//iam.googleapis.com/%s", providerID),
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
TokenURL: "https://sts.googleapis.com/v1/token",
ServiceAccountImpersonationURL: fmt.Sprintf("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", serviceAccount),
CredentialSource: credentialFileSource{
File: upboundProviderIdentityTokenFile,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the credential file does not contain the name of the service account, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ulucinar correct -- the mechanics are somewhat subtle: the file being referenced is the Upbound identity token, and we are using it to construct the GCP federated identity data (which is usually stored as a file, but we are just constructing it in-memory). So the credential source file here is just a JWT.

},
})
}

// TerraformSetupBuilder builds Terraform a terraform.SetupFn function which
// returns Terraform provider setup configuration
func TerraformSetupBuilder(version, providerSource, providerVersion string) terraform.SetupFn {
// NOTE(hasheddan): this function is slightly over our cyclomatic complexity
// goal. Consider refactoring before adding new branches.
func TerraformSetupBuilder(version, providerSource, providerVersion string) terraform.SetupFn { //nolint:gocyclo
return func(ctx context.Context, client client.Client, mg resource.Managed) (terraform.Setup, error) {
ps := terraform.Setup{
Version: version,
Expand Down Expand Up @@ -86,6 +127,15 @@ func TerraformSetupBuilder(version, providerSource, providerVersion string) terr
return ps, errors.Wrap(err, errExtractTokenCredentials)
}
ps.Configuration[keyAccessToken] = string(data)
case credentialsSourceUpbound:
if pc.Spec.Credentials.Upbound == nil || pc.Spec.Credentials.Upbound.Federation == nil {
return ps, errors.Wrap(errors.New(errMissingFederatedConfiguration), errConstructFederatedCredentials)
}
data, err := constructFederatedCredentials(pc.Spec.Credentials.Upbound.Federation.ProviderID, pc.Spec.Credentials.Upbound.Federation.ServiceAccount)
if err != nil {
return ps, errors.Wrap(err, errConstructFederatedCredentials)
}
ps.Configuration[keyCredentials] = string(data)
default:
data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
if err != nil {
Expand Down
24 changes: 24 additions & 0 deletions package/crds/gcp.upbound.io_providerconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,31 @@ spec:
- InjectedIdentity
- Environment
- Filesystem
- Upbound
type: string
upbound:
description: Upbound defines the options for authenticating using
Upbound as an identity provider.
properties:
federation:
description: Federation is the configuration for federated
identity.
properties:
providerID:
description: ProviderID is the fully-qualified identifier
for the identity provider on GCP. The format is `project/<project-id>/locations/global/workloadIdentityPools/<identity-pool>/providers/<identity-provider>`.
minLength: 1
type: string
serviceAccount:
description: ServiceAccount is the email address for the
attached service account.
minLength: 1
type: string
required:
- providerID
- serviceAccount
type: object
type: object
required:
- source
type: object
Expand Down