diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2c046dc8d997c..2a20e39c286d6 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3638,6 +3638,40 @@ const docTemplate = `{ } } }, + "/provisionerkeys/{provisionerkey}": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Fetch provisioner key details", + "operationId": "fetch-provisioner-key-details", + "parameters": [ + { + "type": "string", + "description": "Provisioner Key", + "name": "provisionerkey", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ProvisionerKey" + } + } + } + } + }, "/regions": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4baae0c3568c3..36ea244025cce 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3204,6 +3204,36 @@ } } }, + "/provisionerkeys/{provisionerkey}": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Fetch provisioner key details", + "operationId": "fetch-provisioner-key-details", + "parameters": [ + { + "type": "string", + "description": "Provisioner Key", + "name": "provisionerkey", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ProvisionerKey" + } + } + } + } + }, "/regions": { "get": { "security": [ diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 7b14afbbb285a..12b5ac97565a4 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -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, diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index d5bf44192fc00..f03645ea1e62c 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -2025,6 +2025,50 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Fetch provisioner key details + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/provisionerkeys/{provisionerkey} \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /provisionerkeys/{provisionerkey}` + +### Parameters + +| Name | In | Type | Required | Description | +| ---------------- | ---- | ------ | -------- | --------------- | +| `provisionerkey` | path | string | true | Provisioner Key | + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "organization": "452c1a86-a0af-475b-b03f-724878b0f387", + "tags": { + "property1": "string", + "property2": "string" + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get active replicas ### Code samples diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index b9356bc5b8f92..b6d60b5e4c20e 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -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, diff --git a/enterprise/coderd/provisionerkeys.go b/enterprise/coderd/provisionerkeys.go index 0d715c707b779..a14e2c965c919 100644 --- a/enterprise/coderd/provisionerkeys.go +++ b/enterprise/coderd/provisionerkeys.go @@ -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 +// @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 { diff --git a/enterprise/coderd/provisionerkeys_test.go b/enterprise/coderd/provisionerkeys_test.go index 56ee440131f69..e3f5839bf8b02 100644 --- a/enterprise/coderd/provisionerkeys_test.go +++ b/enterprise/coderd/provisionerkeys_test.go @@ -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) + }) +}