-
Notifications
You must be signed in to change notification settings - Fork 78
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
Changes from all commits
6659498
1c8d13c
6714fc4
b3f4455
ef6607b
46c2bba
a4241ac
446151a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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 { | ||
|
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated!