Skip to content

Commit a518017

Browse files
authored
feat(coderd): add endpoint to fetch provisioner key details (#15505)
This PR is the first step aiming to resolve #15126 - Creating a new endpoint to return the details associated to a provisioner key. This is an authenticated endpoints aiming to be used by the provisioner daemons - using the provisioner key as authentication method. This endpoint is not ment to be used with PSK or User Sessions.
1 parent 593d659 commit a518017

File tree

7 files changed

+305
-8
lines changed

7 files changed

+305
-8
lines changed

coderd/apidoc/docs.go

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codersdk/provisionerdaemons.go

+20
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,26 @@ func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UU
368368
return resp, json.NewDecoder(res.Body).Decode(&resp)
369369
}
370370

371+
// GetProvisionerKey returns the provisioner key.
372+
func (c *Client) GetProvisionerKey(ctx context.Context, pk string) (ProvisionerKey, error) {
373+
res, err := c.Request(ctx, http.MethodGet,
374+
fmt.Sprintf("/api/v2/provisionerkeys/%s", pk), nil,
375+
func(req *http.Request) {
376+
req.Header.Add(ProvisionerDaemonKey, pk)
377+
},
378+
)
379+
if err != nil {
380+
return ProvisionerKey{}, xerrors.Errorf("request to fetch provisioner key failed: %w", err)
381+
}
382+
defer res.Body.Close()
383+
384+
if res.StatusCode != http.StatusOK {
385+
return ProvisionerKey{}, ReadBodyAsError(res)
386+
}
387+
var resp ProvisionerKey
388+
return resp, json.NewDecoder(res.Body).Decode(&resp)
389+
}
390+
371391
// ListProvisionerKeyDaemons lists all provisioner keys with their associated daemons for an organization.
372392
func (c *Client) ListProvisionerKeyDaemons(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKeyDaemons, error) {
373393
res, err := c.Request(ctx, http.MethodGet,

docs/reference/api/enterprise.md

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/coderd/coderd.go

+9
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
343343
r.Get("/", api.groupByOrganization)
344344
})
345345
})
346+
r.Route("/provisionerkeys", func(r chi.Router) {
347+
r.Use(
348+
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
349+
DB: api.Database,
350+
Optional: false,
351+
}),
352+
)
353+
r.Get("/{provisionerkey}", api.fetchProvisionerKey)
354+
})
346355
r.Route("/organizations/{organization}/provisionerkeys", func(r chi.Router) {
347356
r.Use(
348357
apiKeyMiddleware,

enterprise/coderd/provisionerkeys.go

+35-8
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,44 @@ func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) {
200200
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
201201
}
202202

203+
// @Summary Fetch provisioner key details
204+
// @ID fetch-provisioner-key-details
205+
// @Security CoderSessionToken
206+
// @Produce json
207+
// @Tags Enterprise
208+
// @Param provisionerkey path string true "Provisioner Key"
209+
// @Success 200 {object} codersdk.ProvisionerKey
210+
// @Router /provisionerkeys/{provisionerkey} [get]
211+
func (*API) fetchProvisionerKey(rw http.ResponseWriter, r *http.Request) {
212+
ctx := r.Context()
213+
214+
pk, ok := httpmw.ProvisionerKeyAuthOptional(r)
215+
// extra check but this one should never happen as it is covered by the auth middleware
216+
if !ok {
217+
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
218+
Message: fmt.Sprintf("unable to auth: please provide the %s header", codersdk.ProvisionerDaemonKey),
219+
})
220+
return
221+
}
222+
223+
httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerKey(pk))
224+
}
225+
226+
func convertProvisionerKey(dbKey database.ProvisionerKey) codersdk.ProvisionerKey {
227+
return codersdk.ProvisionerKey{
228+
ID: dbKey.ID,
229+
CreatedAt: dbKey.CreatedAt,
230+
OrganizationID: dbKey.OrganizationID,
231+
Name: dbKey.Name,
232+
Tags: codersdk.ProvisionerKeyTags(dbKey.Tags),
233+
// HashedSecret - never include the access token in the API response
234+
}
235+
}
236+
203237
func convertProvisionerKeys(dbKeys []database.ProvisionerKey) []codersdk.ProvisionerKey {
204238
keys := make([]codersdk.ProvisionerKey, 0, len(dbKeys))
205239
for _, dbKey := range dbKeys {
206-
keys = append(keys, codersdk.ProvisionerKey{
207-
ID: dbKey.ID,
208-
CreatedAt: dbKey.CreatedAt,
209-
OrganizationID: dbKey.OrganizationID,
210-
Name: dbKey.Name,
211-
Tags: codersdk.ProvisionerKeyTags(dbKey.Tags),
212-
// HashedSecret - never include the access token in the API response
213-
})
240+
keys = append(keys, convertProvisionerKey(dbKey))
214241
}
215242

216243
slices.SortFunc(keys, func(key1, key2 codersdk.ProvisionerKey) int {

enterprise/coderd/provisionerkeys_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,136 @@ func TestProvisionerKeys(t *testing.T) {
134134
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, codersdk.ProvisionerKeyNamePSK)
135135
require.ErrorContains(t, err, "reserved")
136136
}
137+
138+
func TestGetProvisionerKey(t *testing.T) {
139+
t.Parallel()
140+
141+
tests := []struct {
142+
name string
143+
useFakeKey bool
144+
fakeKey string
145+
success bool
146+
expectedErr string
147+
}{
148+
{
149+
name: "ok",
150+
success: true,
151+
expectedErr: "",
152+
},
153+
{
154+
name: "using unknown key",
155+
useFakeKey: true,
156+
fakeKey: "unknownKey",
157+
success: false,
158+
expectedErr: "provisioner daemon key invalid",
159+
},
160+
{
161+
name: "no key provided",
162+
useFakeKey: true,
163+
fakeKey: "",
164+
success: false,
165+
expectedErr: "provisioner daemon key required",
166+
},
167+
}
168+
169+
for _, tt := range tests {
170+
tt := tt
171+
t.Run(tt.name, func(t *testing.T) {
172+
t.Parallel()
173+
174+
ctx := testutil.Context(t, testutil.WaitShort)
175+
dv := coderdtest.DeploymentValues(t)
176+
client, owner := coderdenttest.New(t, &coderdenttest.Options{
177+
Options: &coderdtest.Options{
178+
DeploymentValues: dv,
179+
},
180+
LicenseOptions: &coderdenttest.LicenseOptions{
181+
Features: license.Features{
182+
codersdk.FeatureMultipleOrganizations: 1,
183+
codersdk.FeatureExternalProvisionerDaemons: 1,
184+
},
185+
},
186+
})
187+
188+
//nolint:gocritic // ignore This client is operating as the owner user, which has unrestricted permissions
189+
key, err := client.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
190+
Name: "my-test-key",
191+
Tags: map[string]string{"key1": "value1", "key2": "value2"},
192+
})
193+
require.NoError(t, err)
194+
195+
pk := key.Key
196+
if tt.useFakeKey {
197+
pk = tt.fakeKey
198+
}
199+
200+
fetchedKey, err := client.GetProvisionerKey(ctx, pk)
201+
if !tt.success {
202+
require.ErrorContains(t, err, tt.expectedErr)
203+
} else {
204+
require.NoError(t, err)
205+
require.Equal(t, fetchedKey.Name, "my-test-key")
206+
require.Equal(t, fetchedKey.Tags, codersdk.ProvisionerKeyTags{"key1": "value1", "key2": "value2"})
207+
}
208+
})
209+
}
210+
211+
t.Run("TestPSK", func(t *testing.T) {
212+
t.Parallel()
213+
const testPSK = "psk-testing-purpose"
214+
ctx := testutil.Context(t, testutil.WaitShort)
215+
dv := coderdtest.DeploymentValues(t)
216+
client, owner := coderdenttest.New(t, &coderdenttest.Options{
217+
ProvisionerDaemonPSK: testPSK,
218+
Options: &coderdtest.Options{
219+
DeploymentValues: dv,
220+
},
221+
LicenseOptions: &coderdenttest.LicenseOptions{
222+
Features: license.Features{
223+
codersdk.FeatureMultipleOrganizations: 1,
224+
codersdk.FeatureExternalProvisionerDaemons: 1,
225+
},
226+
},
227+
})
228+
229+
//nolint:gocritic // ignore This client is operating as the owner user, which has unrestricted permissions
230+
_, err := client.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
231+
Name: "my-test-key",
232+
Tags: map[string]string{"key1": "value1", "key2": "value2"},
233+
})
234+
require.NoError(t, err)
235+
236+
fetchedKey, err := client.GetProvisionerKey(ctx, testPSK)
237+
require.ErrorContains(t, err, "provisioner daemon key invalid")
238+
require.Empty(t, fetchedKey)
239+
})
240+
241+
t.Run("TestSessionToken", func(t *testing.T) {
242+
t.Parallel()
243+
244+
ctx := testutil.Context(t, testutil.WaitShort)
245+
dv := coderdtest.DeploymentValues(t)
246+
client, owner := coderdenttest.New(t, &coderdenttest.Options{
247+
Options: &coderdtest.Options{
248+
DeploymentValues: dv,
249+
},
250+
LicenseOptions: &coderdenttest.LicenseOptions{
251+
Features: license.Features{
252+
codersdk.FeatureMultipleOrganizations: 1,
253+
codersdk.FeatureExternalProvisionerDaemons: 1,
254+
},
255+
},
256+
})
257+
258+
//nolint:gocritic // ignore This client is operating as the owner user, which has unrestricted permissions
259+
_, err := client.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
260+
Name: "my-test-key",
261+
Tags: map[string]string{"key1": "value1", "key2": "value2"},
262+
})
263+
require.NoError(t, err)
264+
265+
fetchedKey, err := client.GetProvisionerKey(ctx, client.SessionToken())
266+
require.ErrorContains(t, err, "provisioner daemon key invalid")
267+
require.Empty(t, fetchedKey)
268+
})
269+
}

0 commit comments

Comments
 (0)