mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
* AGPL Entitlements API Signed-off-by: Spike Curtis <spike@coder.com> * Generate typesGenerated.ts Signed-off-by: Spike Curtis <spike@coder.com> * AllFeatures -> FeatureNames Signed-off-by: Spike Curtis <spike@coder.com> Signed-off-by: Spike Curtis <spike@coder.com>
609 lines
22 KiB
Go
609 lines
22 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/goleak"
|
|
"golang.org/x/xerrors"
|
|
"google.golang.org/api/idtoken"
|
|
"google.golang.org/api/option"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
|
|
"github.com/coder/coder/buildinfo"
|
|
"github.com/coder/coder/coderd"
|
|
"github.com/coder/coder/coderd/autobuild/executor"
|
|
"github.com/coder/coder/coderd/coderdtest"
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/database/databasefake"
|
|
"github.com/coder/coder/coderd/database/postgres"
|
|
"github.com/coder/coder/coderd/gitsshkey"
|
|
"github.com/coder/coder/coderd/rbac"
|
|
"github.com/coder/coder/coderd/telemetry"
|
|
"github.com/coder/coder/coderd/turnconn"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/provisioner/echo"
|
|
"github.com/coder/coder/provisionersdk/proto"
|
|
"github.com/coder/coder/testutil"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
goleak.VerifyTestMain(m)
|
|
}
|
|
|
|
func TestBuildInfo(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, nil)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
buildInfo, err := client.BuildInfo(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, buildinfo.ExternalURL(), buildInfo.ExternalURL, "external URL")
|
|
require.Equal(t, buildinfo.Version(), buildInfo.Version, "version")
|
|
}
|
|
|
|
// TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered.
|
|
func TestAuthorizeAllEndpoints(t *testing.T) {
|
|
t.Parallel()
|
|
authorizer := &recordingAuthorizer{}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// This function was taken from coderdtest.newWithAPI. It is intentionally
|
|
// copied to avoid exposing the API to other tests in coderd. Tests should
|
|
// not need a reference to coderd.API...this test is an exception.
|
|
newClient := func(authorizer rbac.Authorizer) (*codersdk.Client, *coderd.API) {
|
|
// This can be hotswapped for a live database instance.
|
|
db := databasefake.New()
|
|
pubsub := database.NewPubsubInMemory()
|
|
if os.Getenv("DB") != "" {
|
|
connectionURL, closePg, err := postgres.Open()
|
|
require.NoError(t, err)
|
|
t.Cleanup(closePg)
|
|
sqlDB, err := sql.Open("postgres", connectionURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = sqlDB.Close()
|
|
})
|
|
err = database.MigrateUp(sqlDB)
|
|
require.NoError(t, err)
|
|
db = database.New(sqlDB)
|
|
|
|
pubsub, err = database.NewPubsub(ctx, sqlDB, connectionURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = pubsub.Close()
|
|
})
|
|
}
|
|
|
|
tickerCh := make(chan time.Time)
|
|
t.Cleanup(func() { close(tickerCh) })
|
|
|
|
ctx, cancel := context.WithCancel(ctx) // Shadowed to avoid mixing contexts.
|
|
defer t.Cleanup(cancel) // Defer to ensure cancelFunc is executed first.
|
|
|
|
lifecycleExecutor := executor.New(
|
|
ctx,
|
|
db,
|
|
slogtest.Make(t, nil).Named("autobuild.executor").Leveled(slog.LevelDebug),
|
|
tickerCh,
|
|
).WithStatsChannel(nil)
|
|
lifecycleExecutor.Run()
|
|
|
|
srv := httptest.NewUnstartedServer(nil)
|
|
srv.Config.BaseContext = func(_ net.Listener) context.Context {
|
|
return ctx
|
|
}
|
|
srv.Start()
|
|
t.Cleanup(srv.Close)
|
|
serverURL, err := url.Parse(srv.URL)
|
|
require.NoError(t, err)
|
|
|
|
turnServer, err := turnconn.New(nil)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = turnServer.Close()
|
|
})
|
|
|
|
validator, err := idtoken.NewValidator(ctx, option.WithoutAuthentication())
|
|
require.NoError(t, err)
|
|
|
|
// We set the handler after server creation for the access URL.
|
|
coderAPI := coderd.New(&coderd.Options{
|
|
AgentConnectionUpdateFrequency: 150 * time.Millisecond,
|
|
AccessURL: serverURL,
|
|
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
|
|
AWSCertificates: nil,
|
|
AzureCertificates: x509.VerifyOptions{},
|
|
GithubOAuth2Config: nil,
|
|
GoogleTokenValidator: validator,
|
|
SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519,
|
|
TURNServer: turnServer,
|
|
APIRateLimit: 0,
|
|
Authorizer: authorizer,
|
|
Telemetry: telemetry.NewNoop(),
|
|
})
|
|
srv.Config.Handler = coderAPI.Handler
|
|
|
|
_ = coderdtest.NewProvisionerDaemon(t, coderAPI)
|
|
t.Cleanup(func() {
|
|
_ = coderAPI.Close()
|
|
})
|
|
|
|
return codersdk.New(serverURL), coderAPI
|
|
}
|
|
|
|
client, api := newClient(authorizer)
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
// The provisioner will call to coderd and register itself. This is async,
|
|
// so we wait for it to occur.
|
|
require.Eventually(t, func() bool {
|
|
provisionerds, err := client.ProvisionerDaemons(ctx)
|
|
return assert.NoError(t, err) && len(provisionerds) > 0
|
|
}, testutil.WaitLong, testutil.IntervalSlow)
|
|
|
|
provisionerds, err := client.ProvisionerDaemons(ctx)
|
|
require.NoError(t, err, "fetch provisioners")
|
|
require.Len(t, provisionerds, 1)
|
|
|
|
organization, err := client.Organization(ctx, admin.OrganizationID)
|
|
require.NoError(t, err, "fetch org")
|
|
|
|
// Setup some data in the database.
|
|
version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
Provision: []*proto.Provision_Response{{
|
|
Type: &proto.Provision_Response_Complete{
|
|
Complete: &proto.Provision_Complete{
|
|
// Return a workspace resource
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something",
|
|
Auth: &proto.Agent_Token{},
|
|
Apps: []*proto.App{{
|
|
Name: "app",
|
|
Url: "http://localhost:3000",
|
|
}},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, admin.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
|
file, err := client.Upload(ctx, codersdk.ContentTypeTar, make([]byte, 1024))
|
|
require.NoError(t, err, "upload file")
|
|
workspaceResources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
|
|
require.NoError(t, err, "workspace resources")
|
|
templateVersionDryRun, err := client.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{
|
|
ParameterValues: []codersdk.CreateParameterRequest{},
|
|
})
|
|
require.NoError(t, err, "template version dry-run")
|
|
|
|
templateParam, err := client.CreateParameter(ctx, codersdk.ParameterTemplate, template.ID, codersdk.CreateParameterRequest{
|
|
Name: "test-param",
|
|
SourceValue: "hello world",
|
|
SourceScheme: codersdk.ParameterSourceSchemeData,
|
|
DestinationScheme: codersdk.ParameterDestinationSchemeProvisionerVariable,
|
|
})
|
|
require.NoError(t, err, "create template param")
|
|
|
|
// Always fail auth from this point forward
|
|
authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
|
|
|
|
// Some quick reused objects
|
|
workspaceRBACObj := rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(workspace.OwnerID.String())
|
|
workspaceExecObj := rbac.ResourceWorkspaceExecution.InOrg(organization.ID).WithOwner(workspace.OwnerID.String())
|
|
|
|
// skipRoutes allows skipping routes from being checked.
|
|
skipRoutes := map[string]string{
|
|
"POST:/api/v2/users/logout": "Logging out deletes the API Key for other routes",
|
|
}
|
|
|
|
type routeCheck struct {
|
|
NoAuthorize bool
|
|
AssertAction rbac.Action
|
|
AssertObject rbac.Object
|
|
StatusCode int
|
|
}
|
|
assertRoute := map[string]routeCheck{
|
|
// These endpoints do not require auth
|
|
"GET:/api/v2": {NoAuthorize: true},
|
|
"GET:/api/v2/buildinfo": {NoAuthorize: true},
|
|
"GET:/api/v2/users/first": {NoAuthorize: true},
|
|
"POST:/api/v2/users/first": {NoAuthorize: true},
|
|
"POST:/api/v2/users/login": {NoAuthorize: true},
|
|
"GET:/api/v2/users/authmethods": {NoAuthorize: true},
|
|
"POST:/api/v2/csp/reports": {NoAuthorize: true},
|
|
"GET:/api/v2/entitlements": {NoAuthorize: true},
|
|
|
|
"GET:/%40{user}/{workspacename}/apps/{application}/*": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/@{user}/{workspacename}/apps/{application}/*": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
|
|
// Has it's own auth
|
|
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
|
|
"GET:/api/v2/users/oidc/callback": {NoAuthorize: true},
|
|
|
|
// All workspaceagents endpoints do not use rbac
|
|
"POST:/api/v2/workspaceagents/aws-instance-identity": {NoAuthorize: true},
|
|
"POST:/api/v2/workspaceagents/azure-instance-identity": {NoAuthorize: true},
|
|
"POST:/api/v2/workspaceagents/google-instance-identity": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/gitsshkey": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/iceservers": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/listen": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/derp": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/me/wireguardlisten": {NoAuthorize: true},
|
|
"POST:/api/v2/workspaceagents/me/keys": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
|
"GET:/api/v2/workspaceagents/{workspaceagent}/derp": {NoAuthorize: true},
|
|
|
|
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
|
|
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
|
|
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
|
|
"GET:/api/v2/users/{user}/workspace/{workspacename}": {
|
|
AssertObject: rbac.ResourceWorkspace,
|
|
AssertAction: rbac.ActionRead,
|
|
},
|
|
"GET:/api/v2/users/me/workspace/{workspacename}/builds/{buildnumber}": {
|
|
AssertObject: rbac.ResourceWorkspace,
|
|
AssertAction: rbac.ActionRead,
|
|
},
|
|
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspaces/{workspace}/builds": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspaces/{workspace}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"PUT:/api/v2/workspaces/{workspace}/autostart": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"PUT:/api/v2/workspaces/{workspace}/autostop": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspaceresources/{workspaceresource}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspacebuilds/{workspacebuild}/state": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspaceagents/{workspaceagent}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {
|
|
AssertAction: rbac.ActionCreate,
|
|
AssertObject: workspaceExecObj,
|
|
},
|
|
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {
|
|
AssertAction: rbac.ActionCreate,
|
|
AssertObject: workspaceExecObj,
|
|
},
|
|
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {
|
|
AssertAction: rbac.ActionCreate,
|
|
AssertObject: workspaceExecObj,
|
|
},
|
|
"GET:/api/v2/workspaces/": {
|
|
StatusCode: http.StatusOK,
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/organizations/{organization}/templates": {
|
|
StatusCode: http.StatusOK,
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"POST:/api/v2/organizations/{organization}/templates": {
|
|
AssertAction: rbac.ActionCreate,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(organization.ID),
|
|
},
|
|
"DELETE:/api/v2/templates/{template}": {
|
|
AssertAction: rbac.ActionDelete,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templates/{template}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"POST:/api/v2/files": {AssertAction: rbac.ActionCreate, AssertObject: rbac.ResourceFile},
|
|
"GET:/api/v2/files/{fileHash}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceFile.WithOwner(admin.UserID.String()),
|
|
},
|
|
"GET:/api/v2/templates/{template}/versions": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"PATCH:/api/v2/templates/{template}/versions": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templates/{template}/versions/{templateversionname}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"PATCH:/api/v2/templateversions/{templateversion}/cancel": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/logs": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/parameters": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/resources": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/schema": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"POST:/api/v2/templateversions/{templateversion}/dry-run": {
|
|
// The first check is to read the template
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/resources": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID),
|
|
},
|
|
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/logs": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID),
|
|
},
|
|
"PATCH:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/cancel": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(version.OrganizationID),
|
|
},
|
|
"GET:/api/v2/provisionerdaemons": {
|
|
StatusCode: http.StatusOK,
|
|
AssertObject: rbac.ResourceProvisionerDaemon,
|
|
},
|
|
|
|
"POST:/api/v2/parameters/{scope}/{id}": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: rbac.ResourceTemplate,
|
|
},
|
|
"GET:/api/v2/parameters/{scope}/{id}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate,
|
|
},
|
|
"DELETE:/api/v2/parameters/{scope}/{id}/{name}": {
|
|
AssertAction: rbac.ActionUpdate,
|
|
AssertObject: rbac.ResourceTemplate,
|
|
},
|
|
"GET:/api/v2/organizations/{organization}/templates/{templatename}": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID),
|
|
},
|
|
"POST:/api/v2/organizations/{organization}/workspaces": {
|
|
AssertAction: rbac.ActionCreate,
|
|
// No ID when creating
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"GET:/api/v2/workspaces/{workspace}/watch": {
|
|
AssertAction: rbac.ActionRead,
|
|
AssertObject: workspaceRBACObj,
|
|
},
|
|
"POST:/api/v2/users/{user}/organizations": {
|
|
AssertAction: rbac.ActionCreate,
|
|
AssertObject: rbac.ResourceOrganization,
|
|
},
|
|
"GET:/api/v2/users": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceUser},
|
|
|
|
// These endpoints need payloads to get to the auth part. Payloads will be required
|
|
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
|
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
|
|
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
|
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
|
}
|
|
|
|
for k, v := range assertRoute {
|
|
noTrailSlash := strings.TrimRight(k, "/")
|
|
if _, ok := assertRoute[noTrailSlash]; ok && noTrailSlash != k {
|
|
t.Errorf("route %q & %q is declared twice", noTrailSlash, k)
|
|
t.FailNow()
|
|
}
|
|
assertRoute[noTrailSlash] = v
|
|
}
|
|
|
|
for k, v := range skipRoutes {
|
|
noTrailSlash := strings.TrimRight(k, "/")
|
|
if _, ok := skipRoutes[noTrailSlash]; ok && noTrailSlash != k {
|
|
t.Errorf("route %q & %q is declared twice", noTrailSlash, k)
|
|
t.FailNow()
|
|
}
|
|
skipRoutes[noTrailSlash] = v
|
|
}
|
|
|
|
err = chi.Walk(api.Handler, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
|
name := method + ":" + route
|
|
if _, ok := skipRoutes[strings.TrimRight(name, "/")]; ok {
|
|
return nil
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
authorizer.reset()
|
|
routeAssertions, ok := assertRoute[strings.TrimRight(name, "/")]
|
|
if !ok {
|
|
// By default, all omitted routes check for just "authorize" called
|
|
routeAssertions = routeCheck{}
|
|
}
|
|
|
|
// Replace all url params with known values
|
|
route = strings.ReplaceAll(route, "{organization}", admin.OrganizationID.String())
|
|
route = strings.ReplaceAll(route, "{user}", admin.UserID.String())
|
|
route = strings.ReplaceAll(route, "{organizationname}", organization.Name)
|
|
route = strings.ReplaceAll(route, "{workspace}", workspace.ID.String())
|
|
route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String())
|
|
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
|
|
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
|
|
route = strings.ReplaceAll(route, "{workspaceagent}", workspaceResources[0].Agents[0].ID.String())
|
|
route = strings.ReplaceAll(route, "{buildnumber}", strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10))
|
|
route = strings.ReplaceAll(route, "{template}", template.ID.String())
|
|
route = strings.ReplaceAll(route, "{hash}", file.Hash)
|
|
route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())
|
|
route = strings.ReplaceAll(route, "{workspaceapp}", workspaceResources[0].Agents[0].Apps[0].Name)
|
|
route = strings.ReplaceAll(route, "{templateversion}", version.ID.String())
|
|
route = strings.ReplaceAll(route, "{templateversiondryrun}", templateVersionDryRun.ID.String())
|
|
route = strings.ReplaceAll(route, "{templatename}", template.Name)
|
|
// Only checking template scoped params here
|
|
route = strings.ReplaceAll(route, "{scope}", string(templateParam.Scope))
|
|
route = strings.ReplaceAll(route, "{id}", templateParam.ScopeID.String())
|
|
|
|
resp, err := client.Request(ctx, method, route, nil)
|
|
require.NoError(t, err, "do req")
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Logf("Response Body: %q", string(body))
|
|
_ = resp.Body.Close()
|
|
|
|
if !routeAssertions.NoAuthorize {
|
|
assert.NotNil(t, authorizer.Called, "authorizer expected")
|
|
if routeAssertions.StatusCode != 0 {
|
|
assert.Equal(t, routeAssertions.StatusCode, resp.StatusCode, "expect unauthorized")
|
|
} else {
|
|
// It's either a 404 or 403.
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode, "expect unauthorized")
|
|
}
|
|
}
|
|
if authorizer.Called != nil {
|
|
if routeAssertions.AssertAction != "" {
|
|
assert.Equal(t, routeAssertions.AssertAction, authorizer.Called.Action, "resource action")
|
|
}
|
|
if routeAssertions.AssertObject.Type != "" {
|
|
assert.Equal(t, routeAssertions.AssertObject.Type, authorizer.Called.Object.Type, "resource type")
|
|
}
|
|
if routeAssertions.AssertObject.Owner != "" {
|
|
assert.Equal(t, routeAssertions.AssertObject.Owner, authorizer.Called.Object.Owner, "resource owner")
|
|
}
|
|
if routeAssertions.AssertObject.OrgID != "" {
|
|
assert.Equal(t, routeAssertions.AssertObject.OrgID, authorizer.Called.Object.OrgID, "resource org")
|
|
}
|
|
}
|
|
} else {
|
|
assert.Nil(t, authorizer.Called, "authorize not expected")
|
|
}
|
|
})
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
type authCall struct {
|
|
SubjectID string
|
|
Roles []string
|
|
Action rbac.Action
|
|
Object rbac.Object
|
|
}
|
|
|
|
type recordingAuthorizer struct {
|
|
Called *authCall
|
|
AlwaysReturn error
|
|
}
|
|
|
|
func (r *recordingAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
|
|
r.Called = &authCall{
|
|
SubjectID: subjectID,
|
|
Roles: roleNames,
|
|
Action: action,
|
|
Object: object,
|
|
}
|
|
return r.AlwaysReturn
|
|
}
|
|
|
|
func (r *recordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
|
|
return &fakePreparedAuthorizer{
|
|
Original: r,
|
|
SubjectID: subjectID,
|
|
Roles: roles,
|
|
Action: action,
|
|
}, nil
|
|
}
|
|
|
|
func (r *recordingAuthorizer) reset() {
|
|
r.Called = nil
|
|
}
|
|
|
|
type fakePreparedAuthorizer struct {
|
|
Original *recordingAuthorizer
|
|
SubjectID string
|
|
Roles []string
|
|
Action rbac.Action
|
|
}
|
|
|
|
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
|
|
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Action, object)
|
|
}
|