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

Optional configs for setting custom headers / metadata for REST / gRPC operations #18

Merged
merged 7 commits into from
May 6, 2024

Conversation

austin-denoble
Copy link
Contributor

@austin-denoble austin-denoble commented Apr 30, 2024

Problem

Currently, the Go SDK does not support specifying custom headers or metadata for control & data plane operations (REST and gRPC). This is a useful feature to aid in debugging or tracking a specific request, and we'd like to enable this functionality in the Go SDK.

Solution

  • Update NewClientParams and Client structs to support headers, also allow passing a custom RestClient to the new client. This is primarily for allowing mocking in unit tests, but we have a similar approach in other clients allowing users to customize the HTTP module.
  • Add new buildClientOptions function.
  • Update NewClient to support appending Headers and RestClient to clientOptions.
  • Add error message for empty ApiKey without any Authorization header provided. If the user passes both, avoid applying the Api-Key header in favor of the Authorization header.
  • Add new Client.IndexWithAdditionalMetadata constructor.
  • Update IndexConnection struct to include additionalMetadata, and update newIndexConnection to accept additionalMetadata as an argument.
  • Update IndexConnection.akCtx to handle appending the api-key and any additionalMetadata to requests.
  • Update unit tests, add new mocks package and mock_transport to facilitate validating requests made via Client including.

I spent some time trying to figure out how to mock / test IndexConnection but I think it'll be a bit trickier than Client, so somewhat saving for a future PR atm.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Infrastructure change (CI configs, etc)
  • Non-code change (docs, etc)
  • None of the above: (explain here)

Test Plan

just test to run the test suite locally. Validate tests pass in CI.

@austin-denoble austin-denoble changed the title Optional configs to custom headers & metadata for REST & gRPC operations Optional configs for setting custom headers / metadata for REST / gRPC operations Apr 30, 2024
Copy link
Contributor

@haruska haruska left a comment

Choose a reason for hiding this comment

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

Good addition overall. Might want to consider some breaking changes pre-1.0 to clean up some of the options in the constructors.

pinecone/client.go Outdated Show resolved Hide resolved
}

func (c *Client) IndexWithAdditionalMetadata(host string, namespace string, additionalMetadata map[string]string) (*IndexConnection, error) {
idx, err := newIndexConnection(c.apiKey, host, namespace, c.sourceTag, additionalMetadata)
Copy link
Contributor

Choose a reason for hiding this comment

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

The newIndexConnection input is getting a bit long. Consider refactoring this into a breaking change where it takes a struct similar to other constructors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, will go ahead and take a pass on that now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See newIndexParameters struct type which newIndexConnection should consume.

apiKeyProvider, err := securityprovider.NewSecurityProviderApiKey("header", "Api-Key", in.ApiKey)
if err != nil {
return nil, err
}

clientOptions = append(clientOptions, control.WithRequestEditorFn(apiKeyProvider.Intercept))

Copy link
Contributor

Choose a reason for hiding this comment

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

Is an API Key now optional with the addition of custom headers? Still seems required here.

If a caller was attempting to use a JWT Bearer token instead of an API Key for auth, would this now work? Seems I'd need to pass in an API Key (which gets added to the headers) and a custom header with the Bearer token. Also guessing that breaks the request (two auth schemes in the same request headers)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I didn't change anything regarding API key being optional vs. required so I think it still expects it to be populated.

The point about JWT Bearer is intersting, I'd maybe need to defer to @jhamon regarding specifically what we need to support here, but maybe I should look at removing the API key if they've passed a header for auth, for example. 🤔

@@ -66,8 +68,11 @@ func (ts *ClientTests) TestNewClientParamsSet() {
if client.sourceTag != "" {
ts.FailNow(fmt.Sprintf("Expected client to have empty sourceTag, but got '%s'", client.sourceTag))
}
if client.headers != nil {
ts.FailNow(fmt.Sprintf("Expected client to have nil headers, but got '%v'", client.headers))
Copy link
Contributor

@aulorbe aulorbe Apr 30, 2024

Choose a reason for hiding this comment

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

Total nit, but when I read this, I ask myself "what are nil headers?" because nil sounds like a quantity. Maybe rephrase this stmt to be "Expected client headers to be nil".

Copy link
Contributor

Choose a reason for hiding this comment

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

(B/c at least if I'm understanding stuff about Go correctly online, nil isn't really a quantity, it's sorta just a pointer that indicates there's no object or or no value/concrete type, right?)

ts.FailNow(err.Error())
}
if client.apiKey != apiKey {
ts.FailNow(fmt.Sprintf("Expected client to have apiKey '%s', but got '%s'", apiKey, client.apiKey))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this potentially dangerous b/c someone could reveal an API key through this error msg?

Copy link
Contributor

Choose a reason for hiding this comment

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

(I mean, I know it's a mocked API key in the test, but if this function outputs this error msg IRL, that could be a dangerous vulnerability for users, no?)

Copy link
Contributor Author

@austin-denoble austin-denoble Apr 30, 2024

Choose a reason for hiding this comment

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

This is test code and thus not compiled into the actual built package, as far as I understand. Given that and the usage of a fake key in the tests where we're doing this, I feel like this is probably safe. Users would need to be developing the client themselves locally or in CI for it to be an issue I think.

@@ -93,19 +106,47 @@ func (ts *IndexConnectionTests) TestNewIndexConnection() {
if idxConn.Namespace != "" {
ts.FailNow(fmt.Sprintf("Expected idxConn to have empty namespace, but got '%s'", idxConn.Namespace))
}
if idxConn.additionalMetadata != nil {
ts.FailNow(fmt.Sprintf("Expected idxConn to have nil additionalMetadata, but got '%+v'", idxConn.additionalMetadata))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment re: the usage of nil here, as w/my previous comment

Copy link
Contributor

@aulorbe aulorbe left a comment

Choose a reason for hiding this comment

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

Just lurking :) Giving an approval so I don't block!

@austin-denoble austin-denoble force-pushed the adenoble/custom-request-headers branch from 8293e2b to 505a4ff Compare May 3, 2024 21:28
…f both Api-Key and Authorization header have been provided, add unit tests
@austin-denoble austin-denoble force-pushed the adenoble/custom-request-headers branch from 67b7090 to 59a6918 Compare May 3, 2024 21:48
@pinecone-io pinecone-io deleted a comment from gitguardian bot May 3, 2024
@@ -3,7 +3,7 @@ test:
set -o allexport
source .env
set +o allexport
go test -count=1 ./pinecone
go test -count=1 -v ./pinecone
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the additional logging output when running these locally, etc.

Comment on lines 424 to 432
for key, value := range in.Headers {
headerProvider := provider.NewHeaderProvider(key, value)

if strings.Contains(key, "Authorization") {
hasAuthorizationHeader = true
}

clientOptions = append(clientOptions, control.WithRequestEditorFn(headerProvider.Intercept))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any headers we'd want to prevent them from setting? I'm not sure there, just asking the question.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment I don't think there are, good question though I'm also not sure.

Comment on lines +24 to +32
type newIndexParameters struct {
apiKey string
host string
namespace string
sourceTag string
additionalMetadata map[string]string
}

func newIndexConnection(in newIndexParameters) (*IndexConnection, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, I like this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Props to @haruska for the suggestion.

type Metadata = structpb.Struct
type Metadata = structpb.Struct
Copy link
Contributor

Choose a reason for hiding this comment

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

accidental change?

Copy link
Contributor Author

@austin-denoble austin-denoble May 6, 2024

Choose a reason for hiding this comment

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

I think it was just auto-formatting removing the line at the end of the file.

}

type NewClientParams struct {
ApiKey string
SourceTag string // optional
ApiKey string // optional unless no Authorization header provided
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest rephrasing this to "required unless Authorization header provided"

Comment on lines 427 to 429
if strings.Contains(key, "Authorization") {
hasAuthorizationHeader = true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

http headers are typically case in-sensitive so we probably want to relax this check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use strings.ToLower(key), "authorization"), thanks!

Copy link

gitguardian bot commented May 6, 2024

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
Once a secret has been leaked into a git repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Copy link
Contributor

@haruska haruska left a comment

Choose a reason for hiding this comment

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

Good improvements. The creation of the index connection still seems a bit clunky but I think that's just an artifact of the public constructor(s) being off Client. Overall, the PR is much better than the current impl in main.

@austin-denoble austin-denoble merged commit 5a9897e into main May 6, 2024
3 checks passed
@austin-denoble austin-denoble deleted the adenoble/custom-request-headers branch May 6, 2024 20:48
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