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

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codersdk/provisionerdaemons.go

Lines changed: 20 additions & 0 deletions
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

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/coderd/coderd.go

Lines changed: 9 additions & 0 deletions
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

Lines changed: 35 additions & 8 deletions
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

Lines changed: 133 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy