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
43 changes: 35 additions & 8 deletions enterprise/coderd/provisionerkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,44 @@ 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.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("unable to auth: please provide the %s header", codersdk.ProvisionerDaemonKey),
})
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
133 changes: 133 additions & 0 deletions enterprise/coderd/provisionerkeys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,136 @@ func TestProvisionerKeys(t *testing.T) {
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, codersdk.ProvisionerKeyNamePSK)
require.ErrorContains(t, err, "reserved")
}

func TestGetProvisionerKey(t *testing.T) {
t.Parallel()

tests := []struct {
name string
useFakeKey bool
fakeKey string
success bool
expectedErr string
}{
{
name: "ok",
success: true,
expectedErr: "",
},
{
name: "using unknown key",
useFakeKey: true,
fakeKey: "unknownKey",
success: false,
expectedErr: "provisioner daemon key invalid",
},
{
name: "no key provided",
useFakeKey: true,
fakeKey: "",
success: false,
expectedErr: "provisioner daemon key required",
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, 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 // ignore This client is operating as the owner user, which has unrestricted permissions
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)

pk := key.Key
if tt.useFakeKey {
pk = tt.fakeKey
}

fetchedKey, err := client.GetProvisionerKey(ctx, pk)
if !tt.success {
require.ErrorContains(t, err, tt.expectedErr)
} else {
require.NoError(t, err)
require.Equal(t, fetchedKey.Name, "my-test-key")
require.Equal(t, fetchedKey.Tags, codersdk.ProvisionerKeyTags{"key1": "value1", "key2": "value2"})
}
})
}

t.Run("TestPSK", func(t *testing.T) {
t.Parallel()
const testPSK = "psk-testing-purpose"
ctx := testutil.Context(t, testutil.WaitShort)
dv := coderdtest.DeploymentValues(t)
client, owner := coderdenttest.New(t, &coderdenttest.Options{
ProvisionerDaemonPSK: testPSK,
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})

//nolint:gocritic // ignore This client is operating as the owner user, which has unrestricted permissions
_, 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, testPSK)
require.ErrorContains(t, err, "provisioner daemon key invalid")
require.Empty(t, fetchedKey)
})

t.Run("TestSessionToken", 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 // ignore This client is operating as the owner user, which has unrestricted permissions
_, 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, client.SessionToken())
require.ErrorContains(t, err, "provisioner daemon key invalid")
require.Empty(t, fetchedKey)
})
}
Loading