Skip to content

feat(coderd): add endpoint to fetch provisioner key details #15505

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

Merged
merged 13 commits into from
Nov 20, 2024
34 changes: 34 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions codersdk/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,26 @@ func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UU
return resp, json.NewDecoder(res.Body).Decode(&resp)
}

// GetProvisionerKey returns the provisioner key.
func (c *Client) GetProvisionerKey(ctx context.Context, pk string) (ProvisionerKey, error) {
res, err := c.Request(ctx, http.MethodGet,
fmt.Sprintf("/api/v2/provisionerkeys/%s", pk), nil,
func(req *http.Request) {
req.Header.Add(ProvisionerDaemonKey, pk)
},
)
if err != nil {
return ProvisionerKey{}, xerrors.Errorf("request to fetch provisioner key failed: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return ProvisionerKey{}, ReadBodyAsError(res)
}
var resp ProvisionerKey
return resp, json.NewDecoder(res.Body).Decode(&resp)
}

// ListProvisionerKeyDaemons lists all provisioner keys with their associated daemons for an organization.
func (c *Client) ListProvisionerKeyDaemons(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKeyDaemons, error) {
res, err := c.Request(ctx, http.MethodGet,
Expand Down
44 changes: 44 additions & 0 deletions docs/reference/api/enterprise.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Get("/", api.groupByOrganization)
})
})
r.Route("/provisionerkeys", func(r chi.Router) {
r.Use(
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
DB: api.Database,
Optional: false,
}),
)
r.Get("/{provisionerkey}", api.fetchProvisionerKey)
})
r.Route("/organizations/{organization}/provisionerkeys", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
Expand Down
41 changes: 33 additions & 8 deletions enterprise/coderd/provisionerkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,42 @@ func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
}

// @Summary Fetch provisioner key details
// @ID fetch-provisioner-key-details
// @Security CoderSessionToken
Copy link
Member

Choose a reason for hiding this comment

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

This comment should actually be updated as a session token here isn't sufficient auth. Sorry I didn't catch this sooner!

Copy link
Member

Choose a reason for hiding this comment

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

I checked the definition of the /provisionerdaemons/serve endpoint and it looks like it also specifies @Security CoderSessionToken. It looks like that's the only value we allow there currently -- swaggerparser.go enforces this with some special cases for certain endpoints.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed , there's currently some extra logic to change in swaggerparser.go -> https://github.com/coder/coder/blob/main/coderd/coderdtest/swaggerparser.go#L302

I can either keep it like that and create the follow-up issue / PR quickly or add this endpoint to the ignore list for now - as you prefer.

Copy link
Member

Choose a reason for hiding this comment

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

Let's keep it as-is for the moment and create a follow-up PR to address this.

// @Produce json
// @Tags Enterprise
// @Param provisionerkey path string true "Provisioner Key"
// @Success 200 {object} codersdk.ProvisionerKey
// @Router /provisionerkeys/{provisionerkey} [get]
func (*API) fetchProvisionerKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

pk, ok := httpmw.ProvisionerKeyAuthOptional(r)
// extra check but this one should never happen as it is covered by the auth middleware
if !ok {
httpapi.Forbidden(rw)
return
}

httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerKey(pk))
}

func convertProvisionerKey(dbKey database.ProvisionerKey) codersdk.ProvisionerKey {
return codersdk.ProvisionerKey{
ID: dbKey.ID,
CreatedAt: dbKey.CreatedAt,
OrganizationID: dbKey.OrganizationID,
Name: dbKey.Name,
Tags: codersdk.ProvisionerKeyTags(dbKey.Tags),
// HashedSecret - never include the access token in the API response
}
}

func convertProvisionerKeys(dbKeys []database.ProvisionerKey) []codersdk.ProvisionerKey {
keys := make([]codersdk.ProvisionerKey, 0, len(dbKeys))
for _, dbKey := range dbKeys {
keys = append(keys, codersdk.ProvisionerKey{
ID: dbKey.ID,
CreatedAt: dbKey.CreatedAt,
OrganizationID: dbKey.OrganizationID,
Name: dbKey.Name,
Tags: codersdk.ProvisionerKeyTags(dbKey.Tags),
// HashedSecret - never include the access token in the API response
})
keys = append(keys, convertProvisionerKey(dbKey))
}

slices.SortFunc(keys, func(key1, key2 codersdk.ProvisionerKey) int {
Expand Down
38 changes: 38 additions & 0 deletions enterprise/coderd/provisionerkeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,41 @@ func TestProvisionerKeys(t *testing.T) {
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, codersdk.ProvisionerKeyNamePSK)
require.ErrorContains(t, err, "reserved")
}

func TestProvisionerKey(t *testing.T) {
t.Parallel()
t.Run("GetKey", func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitShort)

dv := coderdtest.DeploymentValues(t)
client, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})

// nolint:gocritic
key, err := client.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "my-test-key",
Tags: map[string]string{"key1": "value1", "key2": "value2"},
})
require.NoError(t, err)

fetchedKey, err := client.GetProvisionerKey(ctx, key.Key)
require.NoError(t, err)
require.Equal(t, fetchedKey.Name, "my-test-key")
require.Equal(t, fetchedKey.Tags, codersdk.ProvisionerKeyTags{"key1": "value1", "key2": "value2"})

erroneousPK, err := client.GetProvisionerKey(ctx, "abcdefghijklmnopqrstuvwxyz01234567890123456")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: you're testing multiple scenarios in one test; it's closer to our style to use table tests in this case.
I also think you should add a test to validate the middleware/auth setup.

Copy link
Member

Choose a reason for hiding this comment

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

Suggest testing following scenarios:

  • No auth (403)
  • API key auth (403 with message indicating required header)
  • PSK auth (403 with message indicating required header)
  • Provisioner key auth (200)

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 added some tests as discussed and changed to table tests - should give more coverage on the different cases.

require.Empty(t, erroneousPK)
require.Error(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

})
}
Loading