mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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.
This commit is contained in:
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user