feat: add endpoint for fetching workspace proxy keys (#14789)

This commit is contained in:
Jon Ayers
2024-09-26 21:01:49 +01:00
committed by GitHub
parent 5c977c6be7
commit 3fdeaf7b24
17 changed files with 547 additions and 0 deletions
+1
View File
@@ -243,6 +243,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Post("/app-stats", api.workspaceProxyReportAppStats)
r.Post("/register", api.workspaceProxyRegister)
r.Post("/deregister", api.workspaceProxyDeregister)
r.Get("/crypto-keys", api.workspaceProxyCryptoKeys)
})
r.Route("/{workspaceproxy}", func(r chi.Router) {
r.Use(
+41
View File
@@ -710,6 +710,33 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request)
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
}
// workspaceProxyCryptoKeys is used to fetch signing keys for the workspace proxy.
//
// This is called periodically by the proxy in the background (every 10m per
// replica) to ensure that the proxy has the latest signing keys.
//
// @Summary Get workspace proxy crypto keys
// @ID get-workspace-proxy-crypto-keys
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Success 200 {object} wsproxysdk.CryptoKeysResponse
// @Router /workspaceproxies/me/crypto-keys [get]
// @x-apidocgen {"skip": true}
func (api *API) workspaceProxyCryptoKeys(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
keys, err := api.Database.GetCryptoKeysByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, wsproxysdk.CryptoKeysResponse{
CryptoKeys: fromDBCryptoKeys(keys),
})
}
// @Summary Deregister workspace proxy
// @ID deregister-workspace-proxy
// @Security CoderSessionToken
@@ -967,3 +994,17 @@ func (w *workspaceProxiesFetchUpdater) Fetch(ctx context.Context) (codersdk.Regi
func (w *workspaceProxiesFetchUpdater) Update(ctx context.Context) error {
return w.updateFunc(ctx)
}
func fromDBCryptoKeys(keys []database.CryptoKey) []wsproxysdk.CryptoKey {
wskeys := make([]wsproxysdk.CryptoKey, 0, len(keys))
for _, key := range keys {
wskeys = append(wskeys, wsproxysdk.CryptoKey{
Feature: wsproxysdk.CryptoKeyFeature(key.Feature),
Sequence: key.Sequence,
StartsAt: key.StartsAt.UTC(),
DeletesAt: key.DeletesAt.Time.UTC(),
Secret: key.Secret.String,
})
}
return wskeys
}
+117
View File
@@ -1,6 +1,7 @@
package coderd_test
import (
"database/sql"
"fmt"
"net/http"
"net/http/httptest"
@@ -17,6 +18,7 @@ import (
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/workspaceapps"
@@ -887,3 +889,118 @@ func TestReconnectingPTYSignedToken(t *testing.T) {
// validate it here.
})
}
func TestGetCryptoKeys(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
db, pubsub := dbtestutil.NewDB(t)
cclient, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
Database: db,
Pubsub: pubsub,
IncludeProvisionerDaemon: true,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureWorkspaceProxy: 1,
},
},
})
now := time.Now().UTC()
expectedKey1 := dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceApps,
StartsAt: now.Add(-time.Hour),
Sequence: 2,
})
key1 := fromDBCryptoKeys(expectedKey1)
expectedKey2 := dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceApps,
StartsAt: now,
Sequence: 3,
})
key2 := fromDBCryptoKeys(expectedKey2)
// Create a deleted key.
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceApps,
StartsAt: now.Add(-time.Hour),
Secret: sql.NullString{
String: "secret1",
Valid: false,
},
Sequence: 1,
})
// Create a key with different features.
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureTailnetResume,
StartsAt: now.Add(-time.Hour),
Sequence: 1,
})
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureOidcConvert,
StartsAt: now.Add(-time.Hour),
Sequence: 1,
})
proxy := coderdenttest.NewWorkspaceProxyReplica(t, api, cclient, &coderdenttest.ProxyOptions{
Name: testutil.GetRandomName(t),
})
keys, err := proxy.SDKClient.CryptoKeys(ctx)
require.NoError(t, err)
require.NotEmpty(t, keys)
require.Equal(t, 2, len(keys.CryptoKeys))
require.Contains(t, keys.CryptoKeys, key1)
require.Contains(t, keys.CryptoKeys, key2)
})
t.Run("Unauthorized", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
db, pubsub := dbtestutil.NewDB(t)
cclient, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
Database: db,
Pubsub: pubsub,
IncludeProvisionerDaemon: true,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureWorkspaceProxy: 1,
},
},
})
_ = coderdenttest.NewWorkspaceProxyReplica(t, api, cclient, &coderdenttest.ProxyOptions{
Name: testutil.GetRandomName(t),
})
client := wsproxysdk.New(cclient.URL)
client.SetSessionToken(cclient.SessionToken())
_, err := client.CryptoKeys(ctx)
require.Error(t, err)
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode())
})
}
func fromDBCryptoKeys(key database.CryptoKey) wsproxysdk.CryptoKey {
return wsproxysdk.CryptoKey{
Feature: wsproxysdk.CryptoKeyFeature(key.Feature),
Sequence: key.Sequence,
StartsAt: key.StartsAt.UTC(),
DeletesAt: key.DeletesAt.Time.UTC(),
Secret: key.Secret.String,
}
}