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

replace TokenFile with direct Token in provider configuration #1053

Merged
merged 14 commits into from
Aug 15, 2024

Conversation

liamcervante
Copy link
Member

@liamcervante liamcervante commented Jul 22, 2024

🛠️ Description

For Stacks, we want to support the ability to process the JWT directly instead of loading it via a file. This PR updates the provider configuration so that users can choose to specify the token directly instead of writing it into a file first. The old approach of writing into a file is still available.

@liamcervante liamcervante requested a review from dadgar July 22, 2024 11:38
@liamcervante liamcervante marked this pull request as ready for review August 5, 2024 09:35
@liamcervante liamcervante requested review from a team as code owners August 5, 2024 09:35
Optional: true,
Description: "The path to a file containing a JWT token retrieved from an OpenID Connect (OIDC) or OAuth2 provider. Exactly one of `token_file` or `token` must be set.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use stringvalidator.ConflictsWith or something similar to have TF validate only one is set?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice, didn't know they had such specific validators. I did end up using ExactlyOneOf instead of just ConflictsWith - I think that has the same effect but with an additional validation that one must be set.

@liamcervante liamcervante requested a review from dadgar August 6, 2024 09:43
@liamcervante
Copy link
Member Author

I've tested this locally, and all is working 🎉

docs/index.md Outdated

Optional:

- `token` (String) The JWT token retrieved from an OpenID Connect (OIDC) or OAuth2 provider. Exactly one of `token_file` or `token` must be set.
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the motivation of doing an exactly one?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is two different ways of specifying the same information. Either the token has been written to disk, in which case you should specify the token_file variable, or the token has been loaded dynamically and Terraform just has it, in which case you should specify it directly via token.

It doesn't make sense to specify both of these together. Similarly, you must specify the token somehow if you are authenticating via the block, so you must specify at least one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Letting the user specifying both with a priority would simplify the conditional. I understand the need for at least once. As an example, firebase/firebase-tool: https://github.com/firebase/firebase-tools?tab=readme-ov-file#general

Ultimately, I don't mind having a exactly once, I would like to know the why behind it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've changed this to AtLeastOneOf, and the token field takes precedence over the token_file field.

@@ -122,18 +127,35 @@ func NewClient(config ClientConfig) (*Client, error) {
opts = append(opts, hcpConfig.WithClientCredentials(config.ClientID, config.ClientSecret))
} else if config.CredentialFile != "" {
opts = append(opts, hcpConfig.WithCredentialFilePath(config.CredentialFile))
} else if config.WorloadIdentityTokenFile != "" && config.WorkloadIdentityResourceName != "" {
} else if (config.WorkloadIdentityToken != "" || config.WorkloadIdentityTokenFile != "") && config.WorkloadIdentityResourceName != "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is hard to parse for my out-of-context brain. Is it possible to do it better?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've pulled out the combined or logic into a standalone variable, and added some additional comments explaining the flow. Hopefully, that is better?

@@ -101,15 +101,15 @@ func GetRotatingSecretState(ctx context.Context, client *Client, loc *sharedmode

// CreateMongoDBAtlasRotationIntegration NOTE: currently just needed for tests
func CreateMongoDBAtlasRotationIntegration(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, integrationName, mongodbAtlasPublicKey, mongodbAtlasPrivateKey string) (*secretmodels.Secrets20231128MongoDBAtlasIntegration, error) {
body := secret_service.CreateMongoDBAtlasIntegrationBody{
body := secretmodels.SecretServiceCreateMongoDBAtlasIntegrationBody{
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give some context for those changes?

Copy link
Member Author

Choose a reason for hiding this comment

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

These are changes forced by the hcp-sdk-go upgrade that also added in the new token input when authenticating. The old approach (via the secret_service package) has gone and so wasn't compiling after the upgrade.

There's no functional changes here, the location and names of the various structs have simply been moved around in the referenced library, so the code needs to be updated to point to the new locations.

Copy link
Contributor

@pierluc-codes pierluc-codes left a comment

Choose a reason for hiding this comment

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

I don't see any added or updated test

docs/index.md Outdated

Optional:

- `token` (String) The JWT token retrieved from an OpenID Connect (OIDC) or OAuth2 provider. Exactly one of `token_file` or `token` must be set.
Copy link
Contributor

Choose a reason for hiding this comment

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

Letting the user specifying both with a priority would simplify the conditional. I understand the need for at least once. As an example, firebase/firebase-tool: https://github.com/firebase/firebase-tools?tab=readme-ov-file#general

Ultimately, I don't mind having a exactly once, I would like to know the why behind it.

@@ -116,24 +121,47 @@ type ClientConfig struct {

// NewClient creates a new Client that is capable of making HCP requests
func NewClient(config ClientConfig) (*Client, error) {
hasWorkloadIdentityToken := config.WorkloadIdentityToken != "" || config.WorkloadIdentityTokenFile != ""
Copy link
Contributor

@pierluc-codes pierluc-codes Aug 8, 2024

Choose a reason for hiding this comment

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

I like that approach of add boolean, i think it would be beneficial to add even more which will make your intention more clear and the make the code a little bit more self documenting.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added more in 👍

@liamcervante
Copy link
Member Author

I don't see any added or updated test

This is very difficult to test with an automated process. It's why there weren't any automatic tests for the previous implementation which just supported the token_file input.

We'd need to:

  1. Generate a public/private key pair.
  2. Host and operate an OAuth / OIDC exchange service that publishes the public key.
  3. Configure a HCP account that trusts our new OAuth / OIDC service
  4. Ask for the OAuth / OIDC service to generate a JWT using the private key
  5. Then we can test the provider by inputting that JWT into the token or token_file fields

I've tested this manually by modifying TFC / Atlas / tfc-agent so that I can get a JWT from there which usually isn't possible, and definitely not something we should allow in normal operation. I then built the provider locally, and tested Terraform operations using the JWT I hacked out of TFC with my HCP account set up to trust TFC.

I could add some unit tests for this, but the client actually initiates the token exchange as part of it's initialisation, so we'd need to create some kind of mock or move the token exchange out of initialisation and refactor everything to call the token exchange after.

@pierluc-codes
Copy link
Contributor

This is very difficult to test with an automated process.

I would be minimally happy with only unit test that validate that the expected output are created from the given input.

@liamcervante
Copy link
Member Author

Added unit tests!

@liamcervante liamcervante merged commit ac907e1 into hashicorp:main Aug 15, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants