Skip to content

Commit

Permalink
add actions for api keys
Browse files Browse the repository at this point in the history
  • Loading branch information
haruska committed Mar 27, 2024
1 parent 3a25405 commit b6e9614
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 0 deletions.
203 changes: 203 additions & 0 deletions pinecone/management_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,206 @@ func (c *ManagementClient) DeleteProject(ctx context.Context, projectId uuid.UUI
return fmt.Errorf("unexpected status code: %d", resp.StatusCode())
}
}

// ListApiKeys retrieves all API keys associated with a specific project from the management API.
// It sends a request to the management plane's ListApiKeys endpoint and returns a slice of APIKeyWithoutSecret pointers.
//
// Parameters:
// - ctx: A context.Context to control the request's lifetime.
// - projectId: A UUID representing the unique identifier of the project whose API keys are to be listed.
//
// Returns a slice of pointers to APIKeyWithoutSecret structs populated with the API key data
// on success. In case of failure, it returns an error describing the issue encountered. This could be due
// to unauthorized access, project not found, internal server errors, or other HTTP client errors.
//
// Example:
//
// apiKeys, err := managementClient.ListApiKeys(ctx, projectId)
// if err != nil {
// log.Fatalf("Failed to list API keys: %v", err)
// }
// for _, apiKey := range apiKeys {
// fmt.Printf("API Key ID: %s, Name: %s\n", apiKey.Id, apiKey.Name)
// }
func (c *ManagementClient) ListApiKeys(ctx context.Context, projectId uuid.UUID) ([]*APIKeyWithoutSecret, error) {
resp, err := c.restClient.ListApiKeysWithResponse(ctx, projectId)
if err != nil {
return nil, fmt.Errorf("failed to list API keys: %w", err)
}

// Handle various HTTP response codes and errors
if resp.JSON200 != nil {
apiKeys := make([]*APIKeyWithoutSecret, len(*resp.JSON200.Data))
for i, key := range *resp.JSON200.Data {
apiKeys[i] = &APIKeyWithoutSecret{
Id: key.Id,
Name: key.Name,
ProjectId: key.ProjectId,
}
}
return apiKeys, nil
}

// Detailed error handling based on status code
switch resp.StatusCode() {
case http.StatusUnauthorized:
return nil, fmt.Errorf("unauthorized: %v", resp.JSON401)
case http.StatusInternalServerError:
return nil, fmt.Errorf("internal server error: %v", resp.JSON500)
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode())
}
}

// FetchApiKey retrieves the details of a specific API key by its ID from the management API.
// It sends a request to the management plane's FetchApiKey endpoint and returns the API key details,
// excluding its secret for security reasons. This function is designed to provide information about
// an API key, such as its name and associated project, without compromising sensitive information.
//
// Parameters:
// - ctx: A context.Context to control the request's lifetime, allowing for request cancellation and timeouts.
// - apiKeyId: The UUID representing the unique identifier of the API key to retrieve.
//
// Returns a pointer to an APIKeyWithoutSecret struct populated with the API key's details on success,
// or an error if the operation fails. Possible errors include unauthorized access if the API key used for the
// request doesn't have sufficient permissions, the API key not being found, internal server errors,
// or other HTTP client errors.
//
// Example usage:
//
// apiKeyDetails, err := managementClient.FetchApiKey(ctx, apiKeyId)
// if err != nil {
// log.Fatalf("Failed to fetch API key details: %v", err)
// }
// fmt.Printf("API Key ID: %s, Name: %s\n", apiKeyDetails.Id, apiKeyDetails.Name)
func (c *ManagementClient) FetchApiKey(ctx context.Context, apiKeyId uuid.UUID) (*APIKeyWithoutSecret, error) {
resp, err := c.restClient.FetchApiKeyWithResponse(ctx, apiKeyId)
if err != nil {
return nil, fmt.Errorf("failed to fetch API key: %w", err)
}

switch resp.StatusCode() {
case http.StatusOK:
if resp.JSON200 != nil {
return &APIKeyWithoutSecret{
Id: resp.JSON200.Id,
Name: resp.JSON200.Name,
ProjectId: resp.JSON200.ProjectId,
}, nil
}
case http.StatusUnauthorized:
return nil, fmt.Errorf("unauthorized: %v", resp.JSON401)
case http.StatusNotFound:
return nil, fmt.Errorf("API key not found: %v", resp.JSON404)
case http.StatusInternalServerError:
return nil, fmt.Errorf("internal server error: %v", resp.JSON500)
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode())
}

return nil, fmt.Errorf("unexpected response format or empty data")
}

// CreateApiKey creates a new API key for a given project in the management API.
// It sends a request to the management plane's CreateApiKey endpoint with the necessary
// details and returns the newly created API key's information, including the secret.
//
// This function is critical for enabling secure access to the management and data plane APIs,
// allowing for the creation of scoped access keys associated with specific projects.
//
// Parameters:
// - ctx: A context.Context to control the request's lifetime, enabling request cancellation and timeouts.
// - projectId: The UUID of the project within which the new API key will be created.
// - apiKeyName: A string representing the desired name for the new API key. This name helps identify
// the API key within the project scope and should be unique.
//
// Returns a pointer to an APIKeyWithSecret struct populated with the details of the newly created API key
// on success, or an error if the operation fails. Possible errors include unauthorized access if the provided
// API key does not have sufficient permissions, validation errors for incorrect input values,
// internal server errors, or other HTTP client errors.
//
// Example usage:
//
// newApiKey, err := managementClient.CreateApiKey(ctx, projectId, "NewAPIKeyName")
// if err != nil {
// log.Fatalf("Failed to create API key: %v", err)
// }
// fmt.Printf("Created API Key ID: %s, Name: %s, Project ID: %s, Secret: %s\n",
// newApiKey.Id, newApiKey.Name, newApiKey.ProjectId, newApiKey.Secret)
func (c *ManagementClient) CreateApiKey(ctx context.Context, projectId uuid.UUID, apiKeyName string) (*APIKeyWithSecret, error) {
body := management.CreateApiKeyJSONRequestBody{
Name: apiKeyName,
}

resp, err := c.restClient.CreateApiKeyWithResponse(ctx, projectId, body)
if err != nil {
return nil, fmt.Errorf("failed to create API key: %w", err)
}

switch resp.StatusCode() {
case http.StatusCreated:
if resp.JSON201 != nil {
return &APIKeyWithSecret{
Id: resp.JSON201.Id,
Name: resp.JSON201.Name,
ProjectId: projectId,
Secret: resp.JSON201.Secret,
}, nil
}
case http.StatusUnauthorized:
return nil, fmt.Errorf("unauthorized: %v", resp.JSON401)
case http.StatusBadRequest:
return nil, fmt.Errorf("bad request: %v", resp.JSON400)
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode())
}

return nil, fmt.Errorf("unexpected response format or empty data")
}

// DeleteApiKey deletes an API key by its ID from the management API. This method sends a request
// to the management plane's DeleteApiKey endpoint to permanently remove the specified API key,
// revoking any access it provided.
//
// This operation is essential for managing the lifecycle and security of API keys by allowing
// for the removal of keys that are obsolete or should no longer have access to the API.
//
// Parameters:
// - ctx: A context.Context to control the request's lifetime, enabling features like request cancellation
// and timeouts to handle slow or unresponsive network conditions.
// - apiKeyId: The UUID representing the unique identifier of the API key to be deleted.
//
// Returns an error if the deletion operation fails, providing insight into the failure. Possible
// errors include unauthorized access if the caller lacks sufficient permissions, the specified API key
// not being found, internal server errors, or other HTTP client errors.
//
// Example usage:
//
// err := managementClient.DeleteApiKey(ctx, apiKeyId)
// if err != nil {
// log.Fatalf("Failed to delete API key: %v", err)
// }
func (c *ManagementClient) DeleteApiKey(ctx context.Context, apiKeyId uuid.UUID) error {
resp, err := c.restClient.DeleteApiKeyWithResponse(ctx, apiKeyId)
if err != nil {
return fmt.Errorf("failed to delete API key: %w", err)
}

switch resp.StatusCode() {
case http.StatusOK, http.StatusAccepted, http.StatusNoContent:
// The API key was successfully deleted.
return nil
case http.StatusUnauthorized:
// The request was unauthorized.
return fmt.Errorf("unauthorized: %v", resp.JSON401)
case http.StatusNotFound:
// The specified API key was not found.
return fmt.Errorf("API key not found: %v", resp.JSON404)
case http.StatusInternalServerError:
// An internal server error occurred.
return fmt.Errorf("internal server error: %v", resp.JSON500)
default:
// An unexpected status code was received.
return fmt.Errorf("unexpected status code: %d", resp.StatusCode())
}
}
61 changes: 61 additions & 0 deletions pinecone/management_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/stretchr/testify/suite"
"math/rand"
"os"
"testing"
"time"
Expand All @@ -15,6 +16,7 @@ type ManagementClientTests struct {
suite.Suite
client ManagementClient
project Project
apiKey APIKeyWithoutSecret
}

func TestManagementClient(t *testing.T) {
Expand All @@ -35,6 +37,12 @@ func (ts *ManagementClientTests) SetupSuite() {
require.NoError(ts.T(), err, "Failed to list projects for test setup")
require.Greater(ts.T(), len(projects), 0, "Projects list should not be empty")
ts.project = *projects[0]

apiKeys, err := ts.client.ListApiKeys(context.Background(), ts.project.Id)
require.NoError(ts.T(), err, "Failed to list API keys for test setup")
require.NotNil(ts.T(), apiKeys, "API keys in test setup should not be nil")
require.Greater(ts.T(), len(apiKeys), 0, "API key list in test setup should not be empty")
ts.apiKey = *apiKeys[0]
}

func (ts *ManagementClientTests) TestListProjects() {
Expand Down Expand Up @@ -85,3 +93,56 @@ func (ts *ManagementClientTests) TestDeleteProject() {
_, fetchErr := ts.client.FetchProject(context.Background(), projectToDelete.Id)
require.Error(ts.T(), fetchErr, "Expected an error when fetching a deleted project")
}

func (ts *ManagementClientTests) TestListApiKeys() {
apiKeys, err := ts.client.ListApiKeys(context.Background(), ts.project.Id)
require.NoError(ts.T(), err, "Failed to list API keys")
require.NotNil(ts.T(), apiKeys, "API keys should not be nil")
require.Greater(ts.T(), len(apiKeys), 0, "API key list should not be empty")
}

func (ts *ManagementClientTests) TestFetchApiKey() {
apiKeyDetails, err := ts.client.FetchApiKey(context.Background(), ts.apiKey.Id)
require.NoError(ts.T(), err, "Failed to fetch API key details")
require.NotNil(ts.T(), apiKeyDetails, "API key details should not be nil")
require.Equal(ts.T(), apiKeyDetails.Id, ts.apiKey.Id, "API key ID should match")
require.Equal(ts.T(), apiKeyDetails.Name, ts.apiKey.Name, "API key Name should match")
require.Equal(ts.T(), apiKeyDetails.ProjectId, ts.apiKey.ProjectId, "API key's Project ID should match")
}

func (ts *ManagementClientTests) TestCreateApiKey() {
apiKeyName := generateRandomString(6) // current limitation of Alpha-release
newApiKey, err := ts.client.CreateApiKey(context.Background(), ts.project.Id, apiKeyName)
defer func() {
if newApiKey != nil {
delErr := ts.client.DeleteApiKey(context.Background(), newApiKey.Id)
require.NoError(ts.T(), delErr, "Failed to clean up api key")
}
}()

require.NoError(ts.T(), err, "Failed to create API key")
require.NotNil(ts.T(), newApiKey, "Newly created API key should not be nil")
require.Equal(ts.T(), apiKeyName, newApiKey.Name, "API key name should match")

//current bug in API implementation: "secret" is returned as "value"
//require.NotEmpty(ts.T(), newApiKey.Secret, "Newly created API key should have a secret")
}

func (ts *ManagementClientTests) TestDeleteApiKey() {
// Create an API key to delete
apiKeyName := generateRandomString(6) // current limitation of Alpha-release
apiKey, _ := ts.client.CreateApiKey(context.Background(), ts.project.Id, apiKeyName)
err := ts.client.DeleteApiKey(context.Background(), apiKey.Id)
require.NoError(ts.T(), err, "Failed to delete API key")
}

func generateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz"
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))

b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
35 changes: 35 additions & 0 deletions pinecone/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,38 @@ type Project struct {
Id uuid.UUID
Name string
}

// APIKeyWithoutSecret represents an API key without exposing the secret part.
// It includes the key's ID, name, and the ID of the project it belongs to.
type APIKeyWithoutSecret struct {
// Id is the unique identifier of the API key.
Id uuid.UUID

// Name is the name given to the API key, useful for identification purposes.
Name string

// ProjectId is the ID of the project this API key is associated with.
ProjectId uuid.UUID
}

// APIKeyWithSecret represents an API key along with its secret. This struct is used
// when creating a new API key, where the secret is returned as part of the creation
// process. The secret is sensitive information and should be handled securely, as it
// provides authenticated access to the API.
type APIKeyWithSecret struct {
// Id is the unique identifier of the API key, represented as a UUID.
Id uuid.UUID

// Name is the name given to the API key, useful for identification and organization
// purposes within a project.
Name string

// ProjectId is the UUID of the project to which this API key is associated. It helps
// in scoping the API key to specific projects, enhancing security and manageability.
ProjectId uuid.UUID

// Secret is the sensitive part of the API key, used for authentication in API requests.
// The secret is only exposed to the user upon creation of the API key and should be
// stored securely by the client.
Secret string
}

0 comments on commit b6e9614

Please sign in to comment.