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:
Vincent Vielle
2024-11-20 18:04:47 +01:00
committed by GitHub
parent 593d659ec8
commit a518017a88
7 changed files with 305 additions and 8 deletions
+34
View File
@@ -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": [
+30
View File
@@ -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": [
+20
View File
@@ -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,
+44
View File
@@ -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
+9
View File
@@ -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,
+35 -8
View File
@@ -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 {
+133
View File
@@ -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)
})
}