DELETE license API endpoint (#3697)

* DELETE license API endpoint

Signed-off-by: Spike Curtis <spike@coder.com>

* Fix new lint stuff

Signed-off-by: Spike Curtis <spike@coder.com>

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2022-08-25 14:04:31 -07:00
committed by GitHub
parent 14a9576b77
commit ca3811499e
9 changed files with 229 additions and 40 deletions
+48 -38
View File
@@ -2,6 +2,7 @@ package coderdtest
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
@@ -43,6 +44,7 @@ type AuthTester struct {
File codersdk.UploadResponse
TemplateVersionDryRun codersdk.ProvisionerJob
TemplateParam codersdk.Parameter
URLParams map[string]string
}
func NewAuthTester(ctx context.Context, t *testing.T, options *Options) *AuthTester {
@@ -86,7 +88,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, options *Options) *AuthTes
Id: "something",
Auth: &proto.Agent_Token{},
Apps: []*proto.App{{
Name: "app",
Name: "testapp",
Url: "http://localhost:3000",
}},
}},
@@ -116,6 +118,28 @@ func NewAuthTester(ctx context.Context, t *testing.T, options *Options) *AuthTes
})
require.NoError(t, err, "create template param")
urlParameters := map[string]string{
"{organization}": admin.OrganizationID.String(),
"{user}": admin.UserID.String(),
"{organizationname}": organization.Name,
"{workspace}": workspace.ID.String(),
"{workspacebuild}": workspace.LatestBuild.ID.String(),
"{workspacename}": workspace.Name,
"{workspacebuildname}": workspace.LatestBuild.Name,
"{workspaceagent}": workspaceResources[0].Agents[0].ID.String(),
"{buildnumber}": strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
"{template}": template.ID.String(),
"{hash}": file.Hash,
"{workspaceresource}": workspaceResources[0].ID.String(),
"{workspaceapp}": workspaceResources[0].Agents[0].Apps[0].Name,
"{templateversion}": version.ID.String(),
"{jobID}": templateVersionDryRun.ID.String(),
"{templatename}": template.Name,
// Only checking template scoped params here
"parameters/{scope}/{id}": fmt.Sprintf("parameters/%s/%s",
string(templateParam.Scope), templateParam.ScopeID.String()),
}
return &AuthTester{
t: t,
api: api,
@@ -130,6 +154,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, options *Options) *AuthTes
File: file,
TemplateVersionDryRun: templateVersionDryRun,
TemplateParam: templateParam,
URLParams: urlParameters,
}
}
@@ -153,13 +178,13 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
"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:/%40{user}/{workspacename}/apps/{workspaceapp}/*": {
AssertAction: rbac.ActionCreate,
AssertObject: workspaceExecObj,
},
"GET:/@{user}/{workspacename}/apps/{application}/*": {
AssertAction: rbac.ActionRead,
AssertObject: workspaceRBACObj,
"GET:/@{user}/{workspacename}/apps/{workspaceapp}/*": {
AssertAction: rbac.ActionCreate,
AssertObject: workspaceExecObj,
},
// Has it's own auth
@@ -188,7 +213,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertObject: rbac.ResourceWorkspace,
AssertAction: rbac.ActionRead,
},
"GET:/api/v2/users/me/workspace/{workspacename}/builds/{buildnumber}": {
"GET:/api/v2/users/{user}/workspace/{workspacename}/builds/{buildnumber}": {
AssertObject: rbac.ResourceWorkspace,
AssertAction: rbac.ActionRead,
},
@@ -216,7 +241,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertAction: rbac.ActionUpdate,
AssertObject: workspaceRBACObj,
},
"PUT:/api/v2/workspaces/{workspace}/autostop": {
"PUT:/api/v2/workspaces/{workspace}/ttl": {
AssertAction: rbac.ActionUpdate,
AssertObject: workspaceRBACObj,
},
@@ -275,7 +300,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertObject: rbac.ResourceTemplate.InOrg(a.Template.OrganizationID),
},
"POST:/api/v2/files": {AssertAction: rbac.ActionCreate, AssertObject: rbac.ResourceFile},
"GET:/api/v2/files/{fileHash}": {
"GET:/api/v2/files/{hash}": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceFile.WithOwner(a.Admin.UserID.String()),
},
@@ -320,19 +345,19 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Version.OrganizationID),
},
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}": {
"GET:/api/v2/templateversions/{templateversion}/dry-run/{jobID}": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Version.OrganizationID),
},
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/resources": {
"GET:/api/v2/templateversions/{templateversion}/dry-run/{jobID}/resources": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Version.OrganizationID),
},
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/logs": {
"GET:/api/v2/templateversions/{templateversion}/dry-run/{jobID}/logs": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Version.OrganizationID),
},
"PATCH:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/cancel": {
"PATCH:/api/v2/templateversions/{templateversion}/dry-run/{jobID}/cancel": {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(a.Version.OrganizationID),
},
@@ -366,10 +391,6 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
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
@@ -385,6 +406,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
// Always fail auth from this point forward
a.authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
routeMissing := make(map[string]bool)
for k, v := range assertRoute {
noTrailSlash := strings.TrimRight(k, "/")
if _, ok := assertRoute[noTrailSlash]; ok && noTrailSlash != k {
@@ -392,6 +414,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
a.t.FailNow()
}
assertRoute[noTrailSlash] = v
routeMissing[noTrailSlash] = true
}
for k, v := range skipRoutes {
@@ -422,32 +445,18 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
}
a.t.Run(name, func(t *testing.T) {
a.authorizer.reset()
routeAssertions, ok := assertRoute[strings.TrimRight(name, "/")]
routeKey := strings.TrimRight(name, "/")
routeAssertions, ok := assertRoute[routeKey]
if !ok {
// By default, all omitted routes check for just "authorize" called
routeAssertions = RouteCheck{}
}
delete(routeMissing, routeKey)
// Replace all url params with known values
route = strings.ReplaceAll(route, "{organization}", a.Admin.OrganizationID.String())
route = strings.ReplaceAll(route, "{user}", a.Admin.UserID.String())
route = strings.ReplaceAll(route, "{organizationname}", a.Organization.Name)
route = strings.ReplaceAll(route, "{workspace}", a.Workspace.ID.String())
route = strings.ReplaceAll(route, "{workspacebuild}", a.Workspace.LatestBuild.ID.String())
route = strings.ReplaceAll(route, "{workspacename}", a.Workspace.Name)
route = strings.ReplaceAll(route, "{workspacebuildname}", a.Workspace.LatestBuild.Name)
route = strings.ReplaceAll(route, "{workspaceagent}", a.WorkspaceResource.Agents[0].ID.String())
route = strings.ReplaceAll(route, "{buildnumber}", strconv.FormatInt(int64(a.Workspace.LatestBuild.BuildNumber), 10))
route = strings.ReplaceAll(route, "{template}", a.Template.ID.String())
route = strings.ReplaceAll(route, "{hash}", a.File.Hash)
route = strings.ReplaceAll(route, "{workspaceresource}", a.WorkspaceResource.ID.String())
route = strings.ReplaceAll(route, "{workspaceapp}", a.WorkspaceResource.Agents[0].Apps[0].Name)
route = strings.ReplaceAll(route, "{templateversion}", a.Version.ID.String())
route = strings.ReplaceAll(route, "{templateversiondryrun}", a.TemplateVersionDryRun.ID.String())
route = strings.ReplaceAll(route, "{templatename}", a.Template.Name)
// Only checking template scoped params here
route = strings.ReplaceAll(route, "{scope}", string(a.TemplateParam.Scope))
route = strings.ReplaceAll(route, "{id}", a.TemplateParam.ScopeID.String())
for k, v := range a.URLParams {
route = strings.ReplaceAll(route, k, v)
}
resp, err := a.Client.Request(ctx, method, route, nil)
require.NoError(t, err, "do req")
@@ -486,6 +495,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
return nil
})
require.NoError(a.t, err)
require.Len(a.t, routeMissing, 0, "didn't walk some asserted routes: %v", routeMissing)
}
type authCall struct {
@@ -2301,6 +2301,20 @@ func (q *fakeQuerier) GetLicenses(_ context.Context) ([]database.License, error)
return results, nil
}
func (q *fakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, l := range q.licenses {
if l.ID == id {
q.licenses[index] = q.licenses[len(q.licenses)-1]
q.licenses = q.licenses[:len(q.licenses)-1]
return id, nil
}
}
return 0, sql.ErrNoRows
}
func (q *fakeQuerier) GetUserLinkByLinkedID(_ context.Context, id string) (database.UserLink, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
+1
View File
@@ -21,6 +21,7 @@ type querier interface {
AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error)
DeleteAPIKeyByID(ctx context.Context, id string) error
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
DeleteLicense(ctx context.Context, id int32) (int32, error)
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
+13
View File
@@ -475,6 +475,19 @@ func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyPar
return err
}
const deleteLicense = `-- name: DeleteLicense :one
DELETE
FROM licenses
WHERE id = $1
RETURNING id
`
func (q *sqlQuerier) DeleteLicense(ctx context.Context, id int32) (int32, error) {
row := q.db.QueryRowContext(ctx, deleteLicense, id)
err := row.Scan(&id)
return id, err
}
const getLicenses = `-- name: GetLicenses :many
SELECT id, uploaded_at, jwt, exp
FROM licenses
+6 -1
View File
@@ -8,8 +8,13 @@ INSERT INTO
VALUES
($1, $2, $3) RETURNING *;
-- name: GetLicenses :many
SELECT *
FROM licenses
ORDER BY (id);
-- name: DeleteLicense :one
DELETE
FROM licenses
WHERE id = $1
RETURNING id;