chore: Allow editing proxy fields via api. (#7435)

* chore: Add ability to update workspace proxy fields
This commit is contained in:
Steven Masley
2023-05-09 13:46:50 -05:00
committed by GitHub
parent fc1bc374cb
commit b5ad628460
15 changed files with 886 additions and 68 deletions

View File

@ -120,6 +120,8 @@ func New(ctx context.Context, options *Options) (*API, error) {
httpmw.ExtractWorkspaceProxyParam(api.Database),
)
r.Get("/", api.workspaceProxy)
r.Patch("/", api.patchWorkspaceProxy)
r.Delete("/", api.deleteWorkspaceProxy)
})
})

View File

@ -90,6 +90,79 @@ func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
})
}
// @Summary Update workspace proxy
// @ID update-workspace-proxy
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Enterprise
// @Param workspaceproxy path string true "Proxy ID or name" format(uuid)
// @Param request body codersdk.PatchWorkspaceProxy true "Update workspace proxy request"
// @Success 200 {object} codersdk.WorkspaceProxy
// @Router /workspaceproxies/{workspaceproxy} [patch]
func (api *API) patchWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
proxy = httpmw.WorkspaceProxyParam(r)
auditor = api.AGPL.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
aReq.Old = proxy
defer commitAudit()
var req codersdk.PatchWorkspaceProxy
if !httpapi.Read(ctx, rw, r, &req) {
return
}
var hashedSecret []byte
var fullToken string
if req.RegenerateToken {
var err error
fullToken, hashedSecret, err = generateWorkspaceProxyToken(proxy.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
}
updatedProxy, err := api.Database.UpdateWorkspaceProxy(ctx, database.UpdateWorkspaceProxyParams{
Name: req.Name,
DisplayName: req.DisplayName,
Icon: req.Icon,
ID: proxy.ID,
// If hashedSecret is nil or empty, this will not update the secret.
TokenHashedSecret: hashedSecret,
})
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
aReq.New = updatedProxy
status, ok := api.ProxyHealth.HealthStatus()[updatedProxy.ID]
if !ok {
// The proxy should have some status, but just in case.
status.Status = proxyhealth.Unknown
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateWorkspaceProxyResponse{
Proxy: convertProxy(updatedProxy, status),
ProxyToken: fullToken,
})
// Update the proxy cache.
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
}
// @Summary Delete workspace proxy
// @ID delete-workspace-proxy
// @Security CoderSessionToken
@ -107,7 +180,7 @@ func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionCreate,
Action: database.AuditActionDelete,
})
)
aReq.Old = proxy
@ -135,6 +208,23 @@ func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
}
// @Summary Get workspace proxy
// @ID get-workspace-proxy
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param workspaceproxy path string true "Proxy ID or name" format(uuid)
// @Success 200 {object} codersdk.WorkspaceProxy
// @Router /workspaceproxies/{workspaceproxy} [get]
func (api *API) workspaceProxy(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
proxy = httpmw.WorkspaceProxyParam(r)
)
httpapi.Write(ctx, rw, http.StatusOK, convertProxy(proxy, api.ProxyHealth.HealthStatus()[proxy.ID]))
}
// @Summary Create workspace proxy
// @ID create-workspace-proxy
// @Security CoderSessionToken
@ -177,13 +267,11 @@ func (api *API) postWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
}
id := uuid.New()
secret, err := cryptorand.HexString(64)
fullToken, hashedSecret, err := generateWorkspaceProxyToken(id)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
hashedSecret := sha256.Sum256([]byte(secret))
fullToken := fmt.Sprintf("%s:%s", id, secret)
proxy, err := api.Database.InsertWorkspaceProxy(ctx, database.InsertWorkspaceProxyParams{
ID: id,
@ -206,7 +294,7 @@ func (api *API) postWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
}
aReq.New = proxy
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateWorkspaceProxyResponse{
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.UpdateWorkspaceProxyResponse{
Proxy: convertProxy(proxy, proxyhealth.ProxyStatus{
Proxy: proxy,
CheckedAt: time.Now(),
@ -325,7 +413,20 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request)
var (
ctx = r.Context()
proxy = httpmw.WorkspaceProxy(r)
// TODO: This audit log does not work because it has no user id
// associated with it. The audit log commitAudit() function ignores
// the audit log if there is no user id. We should find a solution
// to make sure this event is tracked.
// auditor = api.AGPL.Auditor.Load()
// aReq, commitAudit = audit.InitRequest[database.WorkspaceProxy](rw, &audit.RequestParams{
// Audit: *auditor,
// Log: api.Logger,
// Request: r,
// Action: database.AuditActionWrite,
//})
)
// aReq.Old = proxy
// defer commitAudit()
var req wsproxysdk.RegisterWorkspaceProxyRequest
if !httpapi.Read(ctx, rw, r, &req) {
@ -364,6 +465,7 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request)
return
}
// aReq.New = updatedProxy
httpapi.Write(ctx, rw, http.StatusCreated, wsproxysdk.RegisterWorkspaceProxyResponse{
AppSecurityKey: api.AppSecurityKey.String(),
})
@ -467,6 +569,16 @@ func (api *API) reconnectingPTYSignedToken(rw http.ResponseWriter, r *http.Reque
})
}
func generateWorkspaceProxyToken(id uuid.UUID) (token string, hashed []byte, err error) {
secret, err := cryptorand.HexString(64)
if err != nil {
return "", nil, xerrors.Errorf("generate token: %w", err)
}
hashedSecret := sha256.Sum256([]byte(secret))
fullToken := fmt.Sprintf("%s:%s", id, secret)
return fullToken, hashedSecret[:], nil
}
func convertProxies(p []database.WorkspaceProxy, statuses map[uuid.UUID]proxyhealth.ProxyStatus) []codersdk.WorkspaceProxy {
resp := make([]codersdk.WorkspaceProxy, 0, len(p))
for _, proxy := range p {
@ -479,6 +591,7 @@ func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) cod
return codersdk.WorkspaceProxy{
ID: p.ID,
Name: p.Name,
DisplayName: p.DisplayName,
Icon: p.Icon,
URL: p.Url,
WildcardHostname: p.WildcardHostname,

View File

@ -177,7 +177,7 @@ func TestRegions(t *testing.T) {
func TestWorkspaceProxyCRUD(t *testing.T) {
t.Parallel()
t.Run("create", func(t *testing.T) {
t.Run("CreateAndUpdate", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
@ -203,14 +203,33 @@ func TestWorkspaceProxyCRUD(t *testing.T) {
})
require.NoError(t, err)
proxies, err := client.WorkspaceProxies(ctx)
found, err := client.WorkspaceProxyByID(ctx, proxyRes.Proxy.ID)
require.NoError(t, err)
require.Len(t, proxies, 1)
require.Equal(t, proxyRes.Proxy.ID, proxies[0].ID)
// This will be different, so set it to the same
found.Status = proxyRes.Proxy.Status
require.Equal(t, proxyRes.Proxy, found, "expected proxy")
require.NotEmpty(t, proxyRes.ProxyToken)
// Update the proxy
expName := namesgenerator.GetRandomName(1)
expDisplayName := namesgenerator.GetRandomName(1)
expIcon := namesgenerator.GetRandomName(1)
_, err = client.PatchWorkspaceProxy(ctx, codersdk.PatchWorkspaceProxy{
ID: proxyRes.Proxy.ID,
Name: expName,
DisplayName: expDisplayName,
Icon: expIcon,
})
require.NoError(t, err, "expected no error updating proxy")
found, err = client.WorkspaceProxyByID(ctx, proxyRes.Proxy.ID)
require.NoError(t, err)
require.Equal(t, expName, found.Name, "name")
require.Equal(t, expDisplayName, found.DisplayName, "display name")
require.Equal(t, expIcon, found.Icon, "icon")
})
t.Run("delete", func(t *testing.T) {
t.Run("Delete", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)