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

Detail and delete api key tenant handlers #116

Merged
merged 2 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 71 additions & 9 deletions pkg/tenant/apikeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (s *Server) ProjectAPIKeyList(c *gin.Context) {
}

// Request a page of API keys from Quarterdeck
// TODO: Handle error status codes returned by Quarterdeck
var reply *qd.APIKeyList
if reply, err = s.quarterdeck.APIKeyList(ctx, req); err != nil {
log.Error().Err(err).Msg("could not list API keys")
Expand All @@ -65,13 +66,9 @@ func (s *Server) ProjectAPIKeyList(c *gin.Context) {
}
for _, key := range reply.APIKeys {
out.APIKeys = append(out.APIKeys, &api.APIKey{
ID: key.ID.String(),
ClientID: key.ClientID,
Name: key.Name,
Owner: key.CreatedBy.String(),
Permissions: key.Permissions,
Created: key.Created.Format(time.RFC3339Nano),
Modified: key.Modified.Format(time.RFC3339Nano),
ID: key.ID.String(),
ClientID: key.ClientID,
Name: key.Name,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

List is only supposed to return some of the fields (see the quarterdeck implementation) so I've updated the handler to reflect that.

})
}

Expand Down Expand Up @@ -136,6 +133,7 @@ func (s *Server) ProjectAPIKeyCreate(c *gin.Context) {
// TODO: Add source to request

// Create the API key with Quarterdeck
// TODO: Handle error status codes returned by Quarterdeck
var key *qd.APIKey
if key, err = s.quarterdeck.APIKeyCreate(ctx, req); err != nil {
log.Error().Err(err).Msg("could not create API key")
Expand Down Expand Up @@ -168,14 +166,78 @@ func (s *Server) APIKeyCreate(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
}

// APIKeyDetail returns details about a specific API key.
//
// Route: GET /v1/apikeys/:apiKeyID
func (s *Server) APIKeyDetail(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
var (
ctx context.Context
err error
)

// User credentials are required to make the Quarterdeck request
if ctx, err = middleware.ContextFromRequest(c); err != nil {
log.Error().Err(err).Msg("could not create user context from request")
c.JSON(http.StatusUnauthorized, api.ErrorResponse("could not fetch credentials for authenticated user"))
return
}

// Parse the API key ID from the URL
apiKeyID := c.Param("apiKeyID")

// Get the API key from Quarterdeck
// TODO: Handle error status codes returned by Quarterdeck
var key *qd.APIKey
if key, err = s.quarterdeck.APIKeyDetail(ctx, apiKeyID); err != nil {
log.Error().Err(err).Str("apiKeyID", apiKeyID).Msg("could not get API key")
c.JSON(http.StatusInternalServerError, api.ErrorResponse("could not retrieve API key"))
return
}

// Return everything but the client secret
out := &api.APIKey{
ID: key.ID.String(),
ClientID: key.ClientID,
Name: key.Name,
Owner: key.CreatedBy.String(),
Permissions: key.Permissions,
Created: key.Created.Format(time.RFC3339Nano),
Modified: key.Modified.Format(time.RFC3339Nano),
}

c.JSON(http.StatusOK, out)
}

func (s *Server) APIKeyUpdate(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
}

// APIKeyDelete deletes an API key by forwarding the request to Quarterdeck.
//
// Route: DELETE /v1/apikeys/:apiKeyID
func (s *Server) APIKeyDelete(c *gin.Context) {
c.JSON(http.StatusNotImplemented, "not implemented yet")
var (
ctx context.Context
err error
)

// User credentials are required to make the Quarterdeck request
if ctx, err = middleware.ContextFromRequest(c); err != nil {
log.Error().Err(err).Msg("could not create user context from request")
c.JSON(http.StatusUnauthorized, api.ErrorResponse("could not fetch credentials for authenticated user"))
return
}

// Parse the API key ID from the URL
apiKeyID := c.Param("apiKeyID")

// Delete the API key using Quarterdeck
// TODO: Handle error status codes returned by Quarterdeck
if err = s.quarterdeck.APIKeyDelete(ctx, apiKeyID); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that quarterdeck will actually return 404 if the API key is not in the user's organization (to prevent users deleting keys from other organizations). However, we should probably be handling the different error cases that get returned from Quarterdeck so I've added some TODOs in this file.

log.Error().Err(err).Str("apiKeyID", apiKeyID).Msg("could not delete API key")
c.JSON(http.StatusInternalServerError, api.ErrorResponse("could not delete API key"))
return
}

c.Status(http.StatusNoContent)
}
150 changes: 115 additions & 35 deletions pkg/tenant/apikeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,19 @@ func (s *tenantTestSuite) TestProjectAPIKeyList() {
page := &qd.APIKeyList{
APIKeys: []*qd.APIKey{
{
ID: ulid.MustParse("01GQ38J5YWH4DCYJ6CZ2P5BA2G"),
ClientID: "ABCDEFGHIJKLMNOP",
ClientSecret: "A1B2C3D4E5F6G7H8I9J0",
Name: "Leopold's Publish Key",
OrgID: ulid.MustParse("01GQ38QWNR7MYQXSQ682PJQM7T"),
ProjectID: ulid.MustParse(projectID),
CreatedBy: ulid.MustParse("01GMTWFK4XZY597Y128KXQ4WHP"),
LastUsed: time.Now(),
Permissions: []string{"publish"},
Created: time.Now(),
Modified: time.Now(),
ID: ulid.MustParse("01GQ38J5YWH4DCYJ6CZ2P5BA2G"),
ClientID: "ABCDEFGHIJKLMNOP",
Name: "Leopold's Publish Key",
},
{
ID: ulid.MustParse("02GQ38J5YWH4DCYJ6CZ2P5BA2G"),
ClientID: "ABCDEFGHIJKLMNOP",
ClientSecret: "A1B2C3D4E5F6G7H8I9J0",
Name: "Leopold's Subscribe Key",
OrgID: ulid.MustParse("01GQ38QWNR7MYQXSQ682PJQM7T"),
ProjectID: ulid.MustParse(projectID),
CreatedBy: ulid.MustParse("01GMTWFK4XZY597Y128KXQ4WHP"),
LastUsed: time.Now(),
Permissions: []string{"subscribe"},
Created: time.Now(),
Modified: time.Now(),
ID: ulid.MustParse("02GQ38J5YWH4DCYJ6CZ2P5BA2G"),
ClientID: "QRSTUVWXYZABCDEF",
Name: "Leopold's Subscribe Key",
},
{
ID: ulid.MustParse("03GQ38J5YWH4DCYJ6CZ2P5BA2G"),
ClientID: "ABCDEFGHIJKLMNOP",
ClientSecret: "A1B2C3D4E5F6G7H8I9J0",
Name: "Leopold's PubSub Key",
OrgID: ulid.MustParse("01GQ38QWNR7MYQXSQ682PJQM7T"),
ProjectID: ulid.MustParse(projectID),
CreatedBy: ulid.MustParse("01GMTWFK4XZY597Y128KXQ4WHP"),
LastUsed: time.Now(),
Permissions: []string{"publish", "subscribe"},
Created: time.Now(),
Modified: time.Now(),
ID: ulid.MustParse("03GQ38J5YWH4DCYJ6CZ2P5BA2G"),
ClientID: "GHIJKLMNOPQRSTUV",
Name: "Leopold's PubSub Key",
},
},
NextPageToken: "next_page_token",
Expand All @@ -76,7 +52,6 @@ func (s *tenantTestSuite) TestProjectAPIKeyList() {
}

// Endpoint must be authenticated
require.NoError(s.SetClientCSRFProtection(), "could not set CSRF protection on client")
_, err := s.client.ProjectAPIKeyList(ctx, "invalid", &api.PageQuery{})
s.requireError(err, http.StatusUnauthorized, "this endpoint requires authentication", "expected error when user is not authenticated")

Expand All @@ -98,8 +73,8 @@ func (s *tenantTestSuite) TestProjectAPIKeyList() {
require.Equal(len(page.APIKeys), len(reply.APIKeys), "expected API key count to match")
for i, key := range reply.APIKeys {
require.Equal(page.APIKeys[i].ID.String(), key.ID, "expected API key ID to match")
require.Equal(page.APIKeys[i].ClientID, key.ClientID, "expected API key Client ID to match")
require.Equal(page.APIKeys[i].Name, key.Name, "expected API key name to match")
require.Equal(page.APIKeys[i].Permissions, key.Permissions, "expected API key permissions to match")
}

// Error should be returned when Quarterdeck returns an error
Expand Down Expand Up @@ -187,3 +162,108 @@ func (s *tenantTestSuite) TestProjectAPIKeyCreate() {
_, err = s.client.ProjectAPIKeyCreate(ctx, projectID, req)
s.requireError(err, http.StatusInternalServerError, "could not create API key", "expected error when quarterdeck returns an error")
}

func (s *tenantTestSuite) TestAPIKeyDetail() {
require := s.Require()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Create initial fixtures
id := "01GQ38J5YWH4DCYJ6CZ2P5DA2G"
orgID := "01GQ38QWNR7MYQXSQ682PJQM7T"
key := &qd.APIKey{
ID: ulid.MustParse(id),
ClientID: "ABCDEFGHIJKLMNOP",
ClientSecret: "A1B2C3D4E5F6G7H8I9J0",
Name: "Leopold's API Key",
OrgID: ulid.MustParse(orgID),
ProjectID: ulid.MustParse("01GQ38J5YWH4DCYJ6CZ2P5BA2G"),
CreatedBy: ulid.MustParse("01GMTWFK4XZY597Y128KXQ4WHP"),
LastUsed: time.Now(),
Permissions: []string{"publish", "subscribe"},
Created: time.Now(),
Modified: time.Now(),
}

// Initial mock checks for an auth token and returns 200 with the key fixture
s.quarterdeck.OnAPIKeys(id, mock.UseStatus(http.StatusOK), mock.UseJSONFixture(key), mock.RequireAuth())

// Create initial user claims
claims := &tokens.Claims{
Name: "Leopold Wentzel",
Email: "leopold.wentzel@gmail.com",
Permissions: []string{"read:nothing"},
OrgID: orgID,
}

// Endpoint must be authenticated
_, err := s.client.APIKeyDetail(ctx, "invalid")
s.requireError(err, http.StatusUnauthorized, "this endpoint requires authentication", "expected error when user is not authenticated")

// User must have the correct permissions
require.NoError(s.SetClientCredentials(claims), "could not set client credentials")
_, err = s.client.APIKeyDetail(ctx, "invalid")
s.requireError(err, http.StatusUnauthorized, "user does not have permission to perform this operation", "expected error when user does not have correct permissions")

// Successfully retrieving an API key
claims.Permissions = []string{perms.ReadAPIKeys}
require.NoError(s.SetClientCredentials(claims), "could not set client credentials")
expected := &api.APIKey{
ID: id,
ClientID: key.ClientID,
Name: key.Name,
Owner: key.CreatedBy.String(),
Permissions: key.Permissions,
Created: key.Created.Format(time.RFC3339Nano),
Modified: key.Modified.Format(time.RFC3339Nano),
}
out, err := s.client.APIKeyDetail(ctx, id)
require.NoError(err, "expected no error when retrieving API key")
require.Equal(expected, out, "expected API key to be retrieved")

// Ensure an error is returned when quarterdeck returns an error
s.quarterdeck.OnAPIKeys(id, mock.UseStatus(http.StatusInternalServerError), mock.RequireAuth())
_, err = s.client.APIKeyDetail(ctx, id)
s.requireError(err, http.StatusInternalServerError, "could not retrieve API key", "expected error when quarterdeck returns an error")
}

func (s *tenantTestSuite) TestAPIKeyDelete() {
require := s.Require()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

id := "01GQ38J5YWH4DCYJ6CZ2P5DA2G"
orgID := "01GQ38QWNR7MYQXSQ682PJQM7T"

// Initial mock checks for an auth token and returns 204
s.quarterdeck.OnAPIKeys(id, mock.UseStatus(http.StatusNoContent), mock.RequireAuth())

// Create initial user claims
claims := &tokens.Claims{
Name: "Leopold Wentzel",
Email: "leopold.wentzel@gmail.com",
Permissions: []string{"delete:nothing"},
OrgID: orgID,
}

// Endpoint must be authenticated
require.NoError(s.SetClientCSRFProtection(), "could not set client CSRF protection")
err := s.client.APIKeyDelete(ctx, "invalid")
s.requireError(err, http.StatusUnauthorized, "this endpoint requires authentication", "expected error when user is not authenticated")

// User must have the correct permissions
require.NoError(s.SetClientCredentials(claims), "could not set client credentials")
err = s.client.APIKeyDelete(ctx, "invalid")
s.requireError(err, http.StatusUnauthorized, "user does not have permission to perform this operation", "expected error when user does not have correct permissions")

// Successfully deleting an API key
claims.Permissions = []string{perms.DeleteAPIKeys}
require.NoError(s.SetClientCredentials(claims), "could not set client credentials")
err = s.client.APIKeyDelete(ctx, id)
require.NoError(err, "expected no error when deleting API key")

// Ensure an error is returned when quarterdeck returns an error
s.quarterdeck.OnAPIKeys(id, mock.UseStatus(http.StatusInternalServerError), mock.RequireAuth())
err = s.client.APIKeyDelete(ctx, id)
s.requireError(err, http.StatusInternalServerError, "could not delete API key", "expected error when quarterdeck returns an error")
}