feat: Refactor API routes to use UUIDs instead of friendly names (#401)

* Add client for agent

* Cleanup code

* Fix linting error

* Rename routes to be simpler

* Rename workspace history to workspace build

* Refactor HTTP middlewares to use UUIDs

* Cleanup routes

* Compiles!

* Fix files and organizations

* Fix querying

* Fix agent lock

* Cleanup database abstraction

* Add parameters

* Fix linting errors

* Fix log race

* Lock on close wait

* Fix log cleanup

* Fix e2e tests

* Fix upstream version of opencensus-go

* Update coderdtest.go

* Fix coverpkg

* Fix codecov ignore
This commit is contained in:
Kyle Carberry
2022-03-07 11:40:54 -06:00
committed by GitHub
parent 330686f60a
commit bf0ae8f573
115 changed files with 5849 additions and 4653 deletions
+1
View File
@@ -27,6 +27,7 @@ func randomAPIKeyParts() (id string, secret string) {
func TestAPIKey(t *testing.T) {
t.Parallel()
successHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Only called if the API key passes through the handler.
httpapi.Write(rw, http.StatusOK, httpapi.Response{
+30
View File
@@ -0,0 +1,30 @@
package httpmw
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/httpapi"
)
// parseUUID consumes a url parameter and parses it as a UUID.
func parseUUID(rw http.ResponseWriter, r *http.Request, param string) (uuid.UUID, bool) {
rawID := chi.URLParam(r, param)
if rawID == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("%s must be provided", param),
})
return uuid.UUID{}, false
}
parsed, err := uuid.Parse(rawID)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("%s must be a uuid", param),
})
return uuid.UUID{}, false
}
return parsed, true
}
+6 -6
View File
@@ -40,18 +40,17 @@ func OrganizationMemberParam(r *http.Request) database.OrganizationMember {
func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
apiKey := APIKey(r)
organizationName := chi.URLParam(r, "organization")
if organizationName == "" {
organizationID := chi.URLParam(r, "organization")
if organizationID == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "organization name must be provided",
Message: "organization must be provided",
})
return
}
organization, err := db.GetOrganizationByName(r.Context(), organizationName)
organization, err := db.GetOrganizationByID(r.Context(), organizationID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("organization %q does not exist", organizationName),
Message: fmt.Sprintf("organization %q does not exist", organizationID),
})
return
}
@@ -61,6 +60,7 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler
})
return
}
apiKey := APIKey(r)
organizationMember, err := db.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: organization.ID,
UserID: apiKey.UserID,
+2 -2
View File
@@ -113,7 +113,7 @@ func TestOrganizationParam(t *testing.T) {
UpdatedAt: database.Now(),
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID)
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
@@ -147,7 +147,7 @@ func TestOrganizationParam(t *testing.T) {
UpdatedAt: database.Now(),
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID)
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
+6 -13
View File
@@ -28,32 +28,25 @@ func ProjectParam(r *http.Request) database.Project {
func ExtractProjectParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
organization := OrganizationParam(r)
projectName := chi.URLParam(r, "project")
if projectName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project name must be provided",
})
projectID, parsed := parseUUID(rw, r, "project")
if !parsed {
return
}
project, err := db.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
OrganizationID: organization.ID,
Name: projectName,
})
project, err := db.GetProjectByID(r.Context(), projectID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("project %q does not exist", projectName),
Message: fmt.Sprintf("project %q does not exist", projectID),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err.Error()),
Message: fmt.Sprintf("get project: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), projectParamContextKey{}, project)
chi.RouteContext(ctx).URLParams.Add("organization", project.OrganizationID)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
+22 -14
View File
@@ -74,7 +74,6 @@ func TestProjectParam(t *testing.T) {
require.NoError(t, err)
ctx := chi.NewRouteContext()
ctx.URLParams.Add("organization", organization.Name)
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, organization
}
@@ -83,11 +82,7 @@ func TestProjectParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
)
rtr.Use(httpmw.ExtractProjectParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
rw := httptest.NewRecorder()
@@ -102,15 +97,11 @@ func TestProjectParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
)
rtr.Use(httpmw.ExtractProjectParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("project", "nothin")
chi.RouteContext(r.Context()).URLParams.Add("project", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@@ -119,14 +110,31 @@ func TestProjectParam(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("BadUUID", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(httpmw.ExtractProjectParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("project", "not-a-uuid")
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("Project", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractOrganizationParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProjectParam(r)
@@ -140,7 +148,7 @@ func TestProjectParam(t *testing.T) {
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("project", project.Name)
chi.RouteContext(r.Context()).URLParams.Add("project", project.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
+5 -18
View File
@@ -8,7 +8,6 @@ import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
@@ -29,27 +28,14 @@ func ProjectVersionParam(r *http.Request) database.ProjectVersion {
func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
project := ProjectParam(r)
projectVersionName := chi.URLParam(r, "projectversion")
if projectVersionName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project version name must be provided",
})
projectVersionID, parsed := parseUUID(rw, r, "projectversion")
if !parsed {
return
}
var projectVersion database.ProjectVersion
uuid, err := uuid.Parse(projectVersionName)
if err == nil {
projectVersion, err = db.GetProjectVersionByID(r.Context(), uuid)
} else {
projectVersion, err = db.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{
ProjectID: project.ID,
Name: projectVersionName,
})
}
projectVersion, err := db.GetProjectVersionByID(r.Context(), projectVersionID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("project version %q does not exist", projectVersionName),
Message: fmt.Sprintf("project version %q does not exist", projectVersionID),
})
return
}
@@ -61,6 +47,7 @@ func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handl
}
ctx := context.WithValue(r.Context(), projectVersionParamContextKey{}, projectVersion)
chi.RouteContext(ctx).URLParams.Add("organization", projectVersion.OrganizationID)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
+8 -19
View File
@@ -90,12 +90,7 @@ func TestProjectVersionParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractProjectVersionParam(db),
)
rtr.Use(httpmw.ExtractProjectVersionParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
rw := httptest.NewRecorder()
@@ -110,16 +105,11 @@ func TestProjectVersionParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractProjectVersionParam(db),
)
rtr.Use(httpmw.ExtractProjectVersionParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("projectversion", "nothin")
chi.RouteContext(r.Context()).URLParams.Add("projectversion", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@@ -134,9 +124,8 @@ func TestProjectVersionParam(t *testing.T) {
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractProjectParam(db),
httpmw.ExtractProjectVersionParam(db),
httpmw.ExtractOrganizationParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProjectVersionParam(r)
@@ -145,12 +134,12 @@ func TestProjectVersionParam(t *testing.T) {
r, project := setupAuthentication(db)
projectVersion, err := db.InsertProjectVersion(context.Background(), database.InsertProjectVersionParams{
ID: uuid.New(),
ProjectID: project.ID,
Name: "moo",
ID: uuid.New(),
OrganizationID: project.OrganizationID,
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.Name)
chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
-64
View File
@@ -1,64 +0,0 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type provisionerJobParamContextKey struct{}
// ProvisionerJobParam returns the project from the ExtractProjectParam handler.
func ProvisionerJobParam(r *http.Request) database.ProvisionerJob {
provisionerJob, ok := r.Context().Value(provisionerJobParamContextKey{}).(database.ProvisionerJob)
if !ok {
panic("developer error: provisioner job param middleware not provided")
}
return provisionerJob
}
// ExtractProvisionerJobParam grabs a provisioner job from the "provisionerjob" URL parameter.
func ExtractProvisionerJobParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
jobID := chi.URLParam(r, "provisionerjob")
if jobID == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "provisioner job must be provided",
})
return
}
jobUUID, err := uuid.Parse(jobID)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "job id must be a uuid",
})
return
}
job, err := db.GetProvisionerJobByID(r.Context(), jobUUID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "job doesn't exist with that id",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), provisionerJobParamContextKey{}, job)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
-109
View File
@@ -1,109 +0,0 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/httpmw"
)
func TestProvisionerJobParam(t *testing.T) {
t.Parallel()
setup := func(db database.Store) (*http.Request, database.ProvisionerJob) {
r := httptest.NewRequest("GET", "/", nil)
provisionerJob, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
ID: uuid.New(),
})
require.NoError(t, err)
ctx := chi.NewRouteContext()
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, provisionerJob
}
t.Run("None", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("BadUUID", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", "nothin")
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("ProvisionerJob", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractProvisionerJobParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.ProvisionerJobParam(r)
rw.WriteHeader(http.StatusOK)
})
r, job := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", job.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}
+65
View File
@@ -0,0 +1,65 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceAgentContextKey struct{}
// WorkspaceAgent returns the workspace agent from the ExtractAgent handler.
func WorkspaceAgent(r *http.Request) database.WorkspaceAgent {
user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent)
if !ok {
panic("developer error: agent middleware not provided")
}
return user
}
// ExtractWorkspaceAgent requires authentication using a valid agent token.
func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(AuthCookie)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("%q cookie must be provided", AuthCookie),
})
return
}
token, err := uuid.Parse(cookie.Value)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("parse token: %s", err),
})
return
}
agent, err := db.GetWorkspaceAgentByAuthToken(r.Context(), token)
if errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "agent token is invalid",
})
return
}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace agent: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), workspaceAgentContextKey{}, agent)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
+73
View File
@@ -0,0 +1,73 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/httpmw"
)
func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
setup := func(db database.Store) (*http.Request, uuid.UUID) {
token := uuid.New()
r := httptest.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{
Name: httpmw.AuthCookie,
Value: token.String(),
})
return r, token
}
t.Run("None", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceAgent(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceAgent(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceAgent(r)
rw.WriteHeader(http.StatusOK)
})
r, token := setup(db)
_, err := db.InsertWorkspaceAgent(context.Background(), database.InsertWorkspaceAgentParams{
ID: uuid.New(),
AuthToken: token,
})
require.NoError(t, err)
require.NoError(t, err)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}
+56
View File
@@ -0,0 +1,56 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceBuildParamContextKey struct{}
// WorkspaceBuildParam returns the workspace build from the ExtractWorkspaceBuildParam handler.
func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild {
workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuild)
if !ok {
panic("developer error: workspace build param middleware not provided")
}
return workspaceBuild
}
// ExtractWorkspaceBuildParam grabs workspace build from the "workspacebuild" URL parameter.
func ExtractWorkspaceBuildParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
workspaceBuildID, parsed := parseUUID(rw, r, "workspacebuild")
if !parsed {
return
}
workspaceBuild, err := db.GetWorkspaceBuildByID(r.Context(), workspaceBuildID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("workspace build %q does not exist", workspaceBuildID),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace build: %s", err.Error()),
})
return
}
ctx := context.WithValue(r.Context(), workspaceBuildParamContextKey{}, workspaceBuild)
// This injects the "workspace" parameter, because it's expected the consumer
// will want to use the Workspace middleware to ensure the caller owns the workspace.
chi.RouteContext(ctx).URLParams.Add("workspace", workspaceBuild.WorkspaceID.String())
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
@@ -19,7 +19,7 @@ import (
"github.com/coder/coder/httpmw"
)
func TestWorkspaceHistoryParam(t *testing.T) {
func TestWorkspaceBuildParam(t *testing.T) {
t.Parallel()
setupAuthentication := func(db database.Store) (*http.Request, database.Workspace) {
@@ -74,12 +74,7 @@ func TestWorkspaceHistoryParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceBuildParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
rw := httptest.NewRecorder()
@@ -94,16 +89,11 @@ func TestWorkspaceHistoryParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceBuildParam(db))
rtr.Get("/", nil)
r, _ := setupAuthentication(db)
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "nothin")
chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@@ -112,60 +102,28 @@ func TestWorkspaceHistoryParam(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("WorkspaceHistory", func(t *testing.T) {
t.Run("WorkspaceBuild", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceBuildParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceHistoryParam(r)
_ = httpmw.WorkspaceBuildParam(r)
rw.WriteHeader(http.StatusOK)
})
r, workspace := setupAuthentication(db)
workspaceHistory, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{
workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{
ID: uuid.New(),
WorkspaceID: workspace.ID,
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", workspaceHistory.Name)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
t.Run("WorkspaceHistoryLatest", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
httpmw.ExtractWorkspaceHistoryParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceHistoryParam(r)
rw.WriteHeader(http.StatusOK)
})
r, workspace := setupAuthentication(db)
_, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{
ID: uuid.New(),
WorkspaceID: workspace.ID,
Name: "moo",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "latest")
chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", workspaceBuild.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
-72
View File
@@ -1,72 +0,0 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceHistoryParamContextKey struct{}
// WorkspaceHistoryParam returns the workspace history from the ExtractWorkspaceHistoryParam handler.
func WorkspaceHistoryParam(r *http.Request) database.WorkspaceHistory {
workspaceHistory, ok := r.Context().Value(workspaceHistoryParamContextKey{}).(database.WorkspaceHistory)
if !ok {
panic("developer error: workspace history param middleware not provided")
}
return workspaceHistory
}
// ExtractWorkspaceHistoryParam grabs workspace history from the "workspacehistory" URL parameter.
func ExtractWorkspaceHistoryParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
workspace := WorkspaceParam(r)
workspaceHistoryName := chi.URLParam(r, "workspacehistory")
if workspaceHistoryName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "workspace history name must be provided",
})
return
}
var workspaceHistory database.WorkspaceHistory
var err error
if workspaceHistoryName == "latest" {
workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "there is no workspace history",
})
return
}
} else {
workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDAndName(r.Context(), database.GetWorkspaceHistoryByWorkspaceIDAndNameParams{
WorkspaceID: workspace.ID,
Name: workspaceHistoryName,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("workspace history %q does not exist", workspaceHistoryName),
})
return
}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace history: %s", err.Error()),
})
return
}
ctx := context.WithValue(r.Context(), workspaceHistoryParamContextKey{}, workspaceHistory)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
+11 -12
View File
@@ -7,8 +7,6 @@ import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
@@ -28,18 +26,11 @@ func WorkspaceParam(r *http.Request) database.Workspace {
func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user := UserParam(r)
workspaceName := chi.URLParam(r, "workspace")
if workspaceName == "" {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "workspace id must be provided",
})
workspaceID, parsed := parseUUID(rw, r, "workspace")
if !parsed {
return
}
workspace, err := db.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: user.ID,
Name: workspaceName,
})
workspace, err := db.GetWorkspaceByID(r.Context(), workspaceID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("workspace %q does not exist", workspace),
@@ -53,6 +44,14 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler {
return
}
apiKey := APIKey(r)
if apiKey.UserID != workspace.OwnerID {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "getting non-personal workspaces isn't supported",
})
return
}
ctx := context.WithValue(r.Context(), workspaceParamContextKey{}, workspace)
next.ServeHTTP(rw, r.WithContext(ctx))
})
+29 -13
View File
@@ -66,11 +66,7 @@ func TestWorkspaceParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceParam(db))
rtr.Get("/", nil)
r, _ := setup(db)
rw := httptest.NewRecorder()
@@ -85,14 +81,10 @@ func TestWorkspaceParam(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Use(httpmw.ExtractWorkspaceParam(db))
rtr.Get("/", nil)
r, _ := setup(db)
chi.RouteContext(r.Context()).URLParams.Add("workspace", "frog")
chi.RouteContext(r.Context()).URLParams.Add("workspace", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
@@ -101,13 +93,37 @@ func TestWorkspaceParam(t *testing.T) {
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("NonPersonal", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db)
workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{
ID: uuid.New(),
OwnerID: "not-me",
Name: "hello",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractAPIKey(db, nil),
httpmw.ExtractUserParam(db),
httpmw.ExtractWorkspaceParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
@@ -121,7 +137,7 @@ func TestWorkspaceParam(t *testing.T) {
Name: "hello",
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.Name)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
+76
View File
@@ -0,0 +1,76 @@
package httpmw
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
)
type workspaceResourceParamContextKey struct{}
// ProvisionerJobParam returns the project from the ExtractProjectParam handler.
func WorkspaceResourceParam(r *http.Request) database.WorkspaceResource {
resource, ok := r.Context().Value(workspaceResourceParamContextKey{}).(database.WorkspaceResource)
if !ok {
panic("developer error: workspace resource param middleware not provided")
}
return resource
}
// ExtractWorkspaceResourceParam grabs a workspace resource from the "provisionerjob" URL parameter.
func ExtractWorkspaceResourceParam(db database.Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
resourceUUID, parsed := parseUUID(rw, r, "workspaceresource")
if !parsed {
return
}
resource, err := db.GetWorkspaceResourceByID(r.Context(), resourceUUID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: "resource doesn't exist with that id",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner resource: %s", err),
})
return
}
job, err := db.GetProvisionerJobByID(r.Context(), resource.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
if job.Type != database.ProvisionerJobTypeWorkspaceBuild {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "Workspace resources can only be fetched for builds.",
})
return
}
build, err := db.GetWorkspaceBuildByJobID(r.Context(), job.ID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace build: %s", err),
})
return
}
ctx := context.WithValue(r.Context(), workspaceResourceParamContextKey{}, resource)
ctx = context.WithValue(ctx, workspaceBuildParamContextKey{}, build)
chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String())
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
+122
View File
@@ -0,0 +1,122 @@
package httpmw_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/httpmw"
)
func TestWorkspaceResourceParam(t *testing.T) {
t.Parallel()
setup := func(db database.Store, jobType database.ProvisionerJobType) (*http.Request, database.WorkspaceResource) {
r := httptest.NewRequest("GET", "/", nil)
job, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
ID: uuid.New(),
Type: jobType,
})
require.NoError(t, err)
workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{
ID: uuid.New(),
JobID: job.ID,
})
require.NoError(t, err)
resource, err := db.InsertWorkspaceResource(context.Background(), database.InsertWorkspaceResourceParams{
ID: uuid.New(),
JobID: job.ID,
})
require.NoError(t, err)
ctx := chi.NewRouteContext()
ctx.URLParams.Add("workspacebuild", workspaceBuild.ID.String())
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
return r, resource
}
t.Run("None", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(httpmw.ExtractWorkspaceResourceParam(db))
rtr.Get("/", nil)
r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild)
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceResourceParam(db),
)
rtr.Get("/", nil)
r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild)
chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", uuid.NewString())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("FoundBadJobType", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceResourceParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceResourceParam(r)
rw.WriteHeader(http.StatusOK)
})
r, job := setup(db, database.ProvisionerJobTypeProjectVersionImport)
chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("Found", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
rtr := chi.NewRouter()
rtr.Use(
httpmw.ExtractWorkspaceResourceParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.WorkspaceResourceParam(r)
rw.WriteHeader(http.StatusOK)
})
r, job := setup(db, database.ProvisionerJobTypeWorkspaceBuild)
chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String())
rw := httptest.NewRecorder()
rtr.ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}