feat: add template RBAC/groups (#4235)

This commit is contained in:
Jon Ayers
2022-10-10 15:37:06 -05:00
committed by GitHub
parent 2687e3db49
commit 3120c94c22
122 changed files with 8088 additions and 1062 deletions

View File

@ -0,0 +1,111 @@
package coderd_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/testutil"
)
func TestCheckACLPermissions(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
adminClient := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
},
})
// Create adminClient, member, and org adminClient
adminUser := coderdtest.CreateFirstUser(t, adminClient)
_ = coderdenttest.AddLicense(t, adminClient, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
memberClient := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
memberUser, err := memberClient.User(ctx, codersdk.Me)
require.NoError(t, err)
orgAdminClient := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID, rbac.RoleOrgAdmin(adminUser.OrganizationID))
orgAdminUser, err := orgAdminClient.User(ctx, codersdk.Me)
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, adminClient, adminUser.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, adminClient, version.ID)
template := coderdtest.CreateTemplate(t, adminClient, adminUser.OrganizationID, version.ID)
err = adminClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
memberUser.ID.String(): codersdk.TemplateRoleAdmin,
},
})
require.NoError(t, err)
const (
updateSpecificTemplate = "read-specific-template"
)
params := map[string]codersdk.AuthorizationCheck{
updateSpecificTemplate: {
Object: codersdk.AuthorizationObject{
ResourceType: rbac.ResourceTemplate.Type,
ResourceID: template.ID.String(),
},
Action: "write",
},
}
testCases := []struct {
Name string
Client *codersdk.Client
UserID uuid.UUID
Check codersdk.AuthorizationResponse
}{
{
Name: "Admin",
Client: adminClient,
UserID: adminUser.UserID,
Check: map[string]bool{
updateSpecificTemplate: true,
},
},
{
Name: "OrgAdmin",
Client: orgAdminClient,
UserID: orgAdminUser.ID,
Check: map[string]bool{
updateSpecificTemplate: true,
},
},
{
Name: "Member",
Client: memberClient,
UserID: memberUser.ID,
Check: map[string]bool{
updateSpecificTemplate: true,
},
},
}
for _, c := range testCases {
c := c
t.Run("CheckAuthorization/"+c.Name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
resp, err := c.Client.CheckAuthorization(ctx, codersdk.AuthorizationRequest{Checks: params})
require.NoError(t, err, "check perms")
require.Equal(t, c.Check, resp)
})
}
}

View File

@ -17,6 +17,7 @@ import (
agplaudit "github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/workspacequota"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/audit"
@ -58,6 +59,36 @@ func New(ctx context.Context, options *Options) (*API, error) {
r.Get("/", api.licenses)
r.Delete("/{id}", api.deleteLicense)
})
r.Route("/organizations/{organization}/groups", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(api.Database),
)
r.Post("/", api.postGroupByOrganization)
r.Get("/", api.groups)
})
r.Route("/templates/{template}/acl", func(r chi.Router) {
r.Use(
api.rbacEnabledMW,
apiKeyMiddleware,
httpmw.ExtractTemplateParam(api.Database),
)
r.Get("/", api.templateACL)
r.Patch("/", api.patchTemplateACL)
})
r.Route("/groups/{group}", func(r chi.Router) {
r.Use(
api.rbacEnabledMW,
apiKeyMiddleware,
httpmw.ExtractGroupParam(api.Database),
)
r.Get("/", api.group)
r.Patch("/", api.patchGroup)
r.Delete("/", api.deleteGroup)
})
r.Route("/workspace-quota", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Route("/{user}", func(r chi.Router) {
@ -92,6 +123,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
type Options struct {
*coderd.Options
RBACEnabled bool
AuditLogging bool
// Whether to block non-browser connections.
BrowserOnly bool
@ -125,6 +157,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
codersdk.FeatureBrowserOnly: api.BrowserOnly,
codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0,
codersdk.FeatureWorkspaceQuota: api.UserWorkspaceQuota != 0,
codersdk.FeatureRBAC: api.RBACEnabled,
})
if err != nil {
return err
@ -244,3 +277,7 @@ func (api *API) runEntitlementsLoop(ctx context.Context) {
}
}
}
func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
return api.AGPL.HTTPAuth.Authorize(r, action, object)
}

View File

@ -41,8 +41,9 @@ func TestEntitlements(t *testing.T) {
})
_ = coderdtest.CreateFirstUser(t, client)
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
UserLimit: 100,
AuditLog: true,
UserLimit: 100,
AuditLog: true,
RBACEnabled: true,
})
res, err := client.Entitlements(context.Background())
require.NoError(t, err)

View File

@ -62,6 +62,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
}
srv, cancelFunc, oop := coderdtest.NewOptions(t, options.Options)
coderAPI, err := coderd.New(context.Background(), &coderd.Options{
RBACEnabled: true,
AuditLogging: options.AuditLogging,
BrowserOnly: options.BrowserOnly,
SCIMAPIKey: options.SCIMAPIKey,
@ -76,6 +77,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
if options.IncludeProvisionerDaemon {
provisionerCloser = coderdtest.NewProvisionerDaemon(t, coderAPI.AGPL)
}
t.Cleanup(func() {
cancelFunc()
_ = provisionerCloser.Close()
@ -96,6 +98,7 @@ type LicenseOptions struct {
BrowserOnly bool
SCIM bool
WorkspaceQuota bool
RBACEnabled bool
}
// AddLicense generates a new license with the options provided and inserts it.
@ -132,6 +135,11 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
workspaceQuota = 1
}
rbac := int64(0)
if options.RBACEnabled {
rbac = 1
}
c := &license.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "test@testing.test",
@ -151,6 +159,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
BrowserOnly: browserOnly,
SCIM: scim,
WorkspaceQuota: workspaceQuota,
RBAC: rbac,
},
}
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c)

View File

@ -6,9 +6,13 @@ import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/testutil"
)
func TestNew(t *testing.T) {
@ -26,10 +30,20 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
IncludeProvisionerDaemon: true,
},
})
ctx, _ := testutil.Context(t)
admin := coderdtest.CreateFirstUser(t, client)
license := coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{})
a := coderdtest.NewAuthTester(context.Background(), t, client, api.AGPL, admin)
license := coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
group, err := client.CreateGroup(ctx, admin.OrganizationID, codersdk.CreateGroupRequest{
Name: "testgroup",
})
require.NoError(t, err)
groupObj := rbac.ResourceGroup.InOrg(admin.OrganizationID)
a := coderdtest.NewAuthTester(ctx, t, client, api.AGPL, admin)
a.URLParams["licenses/{id}"] = fmt.Sprintf("licenses/%d", license.ID)
a.URLParams["groups/{group}"] = fmt.Sprintf("groups/%s", group.ID.String())
skipRoutes, assertRoute := coderdtest.AGPLRoutes(a)
assertRoute["GET:/api/v2/entitlements"] = coderdtest.RouteCheck{
@ -48,6 +62,31 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
AssertAction: rbac.ActionDelete,
AssertObject: rbac.ResourceLicense,
}
assertRoute["GET:/api/v2/templates/{template}/acl"] = coderdtest.RouteCheck{
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate,
}
assertRoute["PATCH:/api/v2/templates/{template}/acl"] = coderdtest.RouteCheck{
AssertAction: rbac.ActionCreate,
AssertObject: rbac.ResourceTemplate,
}
assertRoute["GET:/api/v2/organizations/{organization}/groups"] = coderdtest.RouteCheck{
StatusCode: http.StatusOK,
AssertAction: rbac.ActionRead,
AssertObject: groupObj,
}
assertRoute["PATCH:/api/v2/groups/{group}"] = coderdtest.RouteCheck{
AssertAction: rbac.ActionRead,
AssertObject: groupObj,
}
assertRoute["PATCH:/api/v2/groups/{group}"] = coderdtest.RouteCheck{
AssertAction: rbac.ActionUpdate,
AssertObject: groupObj,
}
assertRoute["DELETE:/api/v2/groups/{group}"] = coderdtest.RouteCheck{
AssertAction: rbac.ActionDelete,
AssertObject: groupObj,
}
a.Test(context.Background(), assertRoute, skipRoutes)
}

318
enterprise/coderd/groups.go Normal file
View File

@ -0,0 +1,318 @@
package coderd
import (
"database/sql"
"fmt"
"net/http"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
)
func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
org = httpmw.OrganizationParam(r)
)
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceGroup) {
http.NotFound(rw, r)
return
}
var req codersdk.CreateGroupRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if req.Name == database.AllUsersGroup {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.AllUsersGroup),
})
return
}
group, err := api.Database.InsertGroup(ctx, database.InsertGroupParams{
ID: uuid.New(),
Name: req.Name,
OrganizationID: org.ID,
})
if database.IsUniqueViolation(err) {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("Group with name %q already exists.", req.Name),
})
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusCreated, convertGroup(group, nil))
}
func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
group = httpmw.GroupParam(r)
)
if !api.Authorize(r, rbac.ActionUpdate, group) {
http.NotFound(rw, r)
return
}
var req codersdk.PatchGroupRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if req.Name != "" && req.Name == database.AllUsersGroup {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("%q is a reserved group name!", database.AllUsersGroup),
})
return
}
users := make([]string, 0, len(req.AddUsers)+len(req.RemoveUsers))
users = append(users, req.AddUsers...)
users = append(users, req.RemoveUsers...)
for _, id := range users {
if _, err := uuid.Parse(id); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("ID %q must be a valid user UUID.", id),
})
return
}
// TODO: It would be nice to enforce this at the schema level
// but unfortunately our org_members table does not have an ID.
_, err := api.Database.GetOrganizationMemberByUserID(ctx, database.GetOrganizationMemberByUserIDParams{
OrganizationID: group.OrganizationID,
UserID: uuid.MustParse(id),
})
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{
Message: fmt.Sprintf("User %q must be a member of organization %q", id, group.ID),
})
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
}
if req.Name != "" {
_, err := api.Database.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
OrganizationID: group.OrganizationID,
Name: req.Name,
})
if err == nil {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("A group with name %q already exists.", req.Name),
})
return
}
}
err := api.Database.InTx(func(tx database.Store) error {
if req.Name != "" {
var err error
group, err = tx.UpdateGroupByID(ctx, database.UpdateGroupByIDParams{
ID: group.ID,
Name: req.Name,
})
if err != nil {
return xerrors.Errorf("update group by ID: %w", err)
}
}
for _, id := range req.AddUsers {
err := tx.InsertGroupMember(ctx, database.InsertGroupMemberParams{
GroupID: group.ID,
UserID: uuid.MustParse(id),
})
if err != nil {
return xerrors.Errorf("insert group member %q: %w", id, err)
}
}
for _, id := range req.RemoveUsers {
err := tx.DeleteGroupMember(ctx, uuid.MustParse(id))
if err != nil {
return xerrors.Errorf("insert group member %q: %w", id, err)
}
}
return nil
})
if database.IsUniqueViolation(err) {
httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{
Message: "Cannot add the same user to a group twice!",
Detail: err.Error(),
})
return
}
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{
Message: "Failed to add or remove non-existent group member",
Detail: err.Error(),
})
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
members, err := api.Database.GetGroupMembers(ctx, group.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertGroup(group, members))
}
func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
group = httpmw.GroupParam(r)
)
if !api.Authorize(r, rbac.ActionDelete, group) {
httpapi.ResourceNotFound(rw)
return
}
if group.Name == database.AllUsersGroup {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("%q is a reserved group and cannot be deleted!", database.AllUsersGroup),
})
return
}
err := api.Database.DeleteGroupByID(ctx, group.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Successfully deleted group!",
})
}
func (api *API) group(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
group = httpmw.GroupParam(r)
)
if !api.Authorize(r, rbac.ActionRead, group) {
httpapi.ResourceNotFound(rw)
return
}
users, err := api.Database.GetGroupMembers(ctx, group.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertGroup(group, users))
}
func (api *API) groups(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
org = httpmw.OrganizationParam(r)
)
groups, err := api.Database.GetGroupsByOrganizationID(ctx, org.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.InternalServerError(rw, err)
return
}
// Filter groups based on rbac permissions
groups, err = coderd.AuthorizeFilter(api.AGPL.HTTPAuth, r, rbac.ActionRead, groups)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching groups.",
Detail: err.Error(),
})
return
}
resp := make([]codersdk.Group, 0, len(groups))
for _, group := range groups {
members, err := api.Database.GetGroupMembers(ctx, group.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
resp = append(resp, convertGroup(group, members))
}
httpapi.Write(ctx, rw, http.StatusOK, resp)
}
func convertGroup(g database.Group, users []database.User) codersdk.Group {
// It's ridiculous to query all the orgs of a user here
// especially since as of the writing of this comment there
// is only one org. So we pretend everyone is only part of
// the group's organization.
orgs := make(map[uuid.UUID][]uuid.UUID)
for _, user := range users {
orgs[user.ID] = []uuid.UUID{g.OrganizationID}
}
return codersdk.Group{
ID: g.ID,
Name: g.Name,
OrganizationID: g.OrganizationID,
Members: convertUsers(users, orgs),
}
}
func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
convertedUser := codersdk.User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
LastSeenAt: user.LastSeenAt,
Username: user.Username,
Status: codersdk.UserStatus(user.Status),
OrganizationIDs: organizationIDs,
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
AvatarURL: user.AvatarURL.String,
}
for _, roleName := range user.RBACRoles {
rbacRole, _ := rbac.RoleByName(roleName)
convertedUser.Roles = append(convertedUser.Roles, convertRole(rbacRole))
}
return convertedUser
}
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
converted := make([]codersdk.User, 0, len(users))
for _, u := range users {
userOrganizationIDs := organizationIDsByUserID[u.ID]
converted = append(converted, convertUser(u, userOrganizationIDs))
}
return converted
}
func convertRole(role rbac.Role) codersdk.Role {
return codersdk.Role{
DisplayName: role.DisplayName,
Name: role.Name,
}
}

View File

@ -0,0 +1,504 @@
package coderd_test
import (
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/testutil"
)
func TestCreateGroup(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
require.Equal(t, "hi", group.Name)
require.Empty(t, group.Members)
require.NotEqual(t, uuid.Nil.String(), group.ID.String())
})
t.Run("Conflict", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
_, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
_, err = client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusConflict, cerr.StatusCode())
})
t.Run("allUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
_, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: database.AllUsersGroup,
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
}
func TestPatchGroup(t *testing.T) {
t.Parallel()
t.Run("Name", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
Name: "bye",
})
require.NoError(t, err)
require.Equal(t, "bye", group.Name)
})
t.Run("AddUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user2.ID.String(), user3.ID.String()},
})
require.NoError(t, err)
require.Contains(t, group.Members, user2)
require.Contains(t, group.Members, user3)
})
t.Run("RemoveUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user4 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user2.ID.String(), user3.ID.String(), user4.ID.String()},
})
require.NoError(t, err)
require.Contains(t, group.Members, user2)
require.Contains(t, group.Members, user3)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
RemoveUsers: []string{user2.ID.String(), user3.ID.String()},
})
require.NoError(t, err)
require.NotContains(t, group.Members, user2)
require.NotContains(t, group.Members, user3)
require.Contains(t, group.Members, user4)
})
t.Run("UserNotExist", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{uuid.NewString()},
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusPreconditionFailed, cerr.StatusCode())
})
t.Run("MalformedUUID", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{"yeet"},
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
t.Run("AddDuplicateUser", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user2.ID.String(), user2.ID.String()},
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusPreconditionFailed, cerr.StatusCode())
})
t.Run("allUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
Name: database.AllUsersGroup,
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
}
// TODO: test auth.
func TestGroup(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
ggroup, err := client.Group(ctx, group.ID)
require.NoError(t, err)
require.Equal(t, group, ggroup)
})
t.Run("WithUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user2.ID.String(), user3.ID.String()},
})
require.NoError(t, err)
require.Contains(t, group.Members, user2)
require.Contains(t, group.Members, user3)
ggroup, err := client.Group(ctx, group.ID)
require.NoError(t, err)
require.Equal(t, group, ggroup)
})
t.Run("RegularUserReadGroup", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client1, _ := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
ggroup, err := client1.Group(ctx, group.ID)
require.NoError(t, err)
require.Equal(t, group, ggroup)
})
t.Run("FilterDeletedUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user1.ID.String(), user2.ID.String()},
})
require.NoError(t, err)
require.Contains(t, group.Members, user1)
require.Contains(t, group.Members, user2)
err = client.DeleteUser(ctx, user1.ID)
require.NoError(t, err)
group, err = client.Group(ctx, group.ID)
require.NoError(t, err)
require.NotContains(t, group.Members, user1)
})
t.Run("FilterSuspendedUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user1.ID.String(), user2.ID.String()},
})
require.NoError(t, err)
require.Len(t, group.Members, 2)
require.Contains(t, group.Members, user1)
require.Contains(t, group.Members, user2)
user1, err = client.UpdateUserStatus(ctx, user1.ID.String(), codersdk.UserStatusSuspended)
require.NoError(t, err)
group, err = client.Group(ctx, group.ID)
require.NoError(t, err)
require.Len(t, group.Members, 1)
require.NotContains(t, group.Members, user1)
require.Contains(t, group.Members, user2)
})
}
// TODO: test auth.
func TestGroups(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user4 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user5 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
ctx, _ := testutil.Context(t)
group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
group2, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hey",
})
require.NoError(t, err)
group1, err = client.PatchGroup(ctx, group1.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user2.ID.String(), user3.ID.String()},
})
require.NoError(t, err)
group2, err = client.PatchGroup(ctx, group2.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user4.ID.String(), user5.ID.String()},
})
require.NoError(t, err)
groups, err := client.GroupsByOrganization(ctx, user.OrganizationID)
require.NoError(t, err)
require.Len(t, groups, 2)
require.Contains(t, groups, group1)
require.Contains(t, groups, group2)
})
}
func TestDeleteGroup(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)
err = client.DeleteGroup(ctx, group1.ID)
require.NoError(t, err)
_, err = client.Group(ctx, group1.ID)
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})
t.Run("allUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
ctx, _ := testutil.Context(t)
err := client.DeleteGroup(ctx, user.OrganizationID)
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
}

View File

@ -96,6 +96,12 @@ func Entitlements(ctx context.Context, db database.Store, logger slog.Logger, ke
Enabled: enablements[codersdk.FeatureWorkspaceQuota],
}
}
if claims.Features.RBAC > 0 {
entitlements.Features[codersdk.FeatureRBAC] = codersdk.Feature{
Entitlement: entitlement,
Enabled: enablements[codersdk.FeatureRBAC],
}
}
if claims.AllFeatures {
allFeatures = true
}
@ -170,6 +176,7 @@ type Features struct {
BrowserOnly int64 `json:"browser_only"`
SCIM int64 `json:"scim"`
WorkspaceQuota int64 `json:"workspace_quota"`
RBAC int64 `json:"rbac"`
}
type Claims struct {

View File

@ -24,6 +24,7 @@ func TestEntitlements(t *testing.T) {
codersdk.FeatureBrowserOnly: true,
codersdk.FeatureSCIM: true,
codersdk.FeatureWorkspaceQuota: true,
codersdk.FeatureRBAC: true,
}
t.Run("Defaults", func(t *testing.T) {
@ -64,6 +65,7 @@ func TestEntitlements(t *testing.T) {
BrowserOnly: true,
SCIM: true,
WorkspaceQuota: true,
RBACEnabled: true,
}),
Exp: time.Now().Add(time.Hour),
})
@ -85,6 +87,7 @@ func TestEntitlements(t *testing.T) {
BrowserOnly: true,
SCIM: true,
WorkspaceQuota: true,
RBACEnabled: true,
GraceAt: time.Now().Add(-time.Hour),
ExpiresAt: time.Now().Add(time.Hour),
}),

View File

@ -82,6 +82,7 @@ func TestGetLicense(t *testing.T) {
AuditLog: true,
SCIM: true,
BrowserOnly: true,
RBACEnabled: true,
})
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
@ -91,6 +92,7 @@ func TestGetLicense(t *testing.T) {
BrowserOnly: true,
Trial: true,
UserLimit: 200,
RBACEnabled: false,
})
licenses, err := client.Licenses(ctx)
@ -104,6 +106,7 @@ func TestGetLicense(t *testing.T) {
codersdk.FeatureSCIM: json.Number("1"),
codersdk.FeatureBrowserOnly: json.Number("1"),
codersdk.FeatureWorkspaceQuota: json.Number("0"),
codersdk.FeatureRBAC: json.Number("1"),
}, licenses[0].Claims["features"])
assert.Equal(t, int32(2), licenses[1].ID)
assert.Equal(t, "testing2", licenses[1].Claims["account_id"])
@ -114,6 +117,7 @@ func TestGetLicense(t *testing.T) {
codersdk.FeatureSCIM: json.Number("1"),
codersdk.FeatureBrowserOnly: json.Number("1"),
codersdk.FeatureWorkspaceQuota: json.Number("0"),
codersdk.FeatureRBAC: json.Number("0"),
}, licenses[1].Claims["features"])
})
}

View File

@ -0,0 +1,262 @@
package coderd
import (
"context"
"database/sql"
"fmt"
"net/http"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
)
func (api *API) templateACL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)
if !api.Authorize(r, rbac.ActionRead, template) {
httpapi.ResourceNotFound(rw)
return
}
users, err := api.Database.GetTemplateUserRoles(ctx, template.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
dbGroups, err := api.Database.GetTemplateGroupRoles(ctx, template.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
dbGroups, err = coderd.AuthorizeFilter(api.AGPL.HTTPAuth, r, rbac.ActionRead, dbGroups)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching users.",
Detail: err.Error(),
})
return
}
userIDs := make([]uuid.UUID, 0, len(users))
for _, user := range users {
userIDs = append(userIDs, user.ID)
}
orgIDsByMemberIDsRows, err := api.Database.GetOrganizationIDsByMemberIDs(r.Context(), userIDs)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.InternalServerError(rw, err)
return
}
organizationIDsByUserID := map[uuid.UUID][]uuid.UUID{}
for _, organizationIDsByMemberIDsRow := range orgIDsByMemberIDsRows {
organizationIDsByUserID[organizationIDsByMemberIDsRow.UserID] = organizationIDsByMemberIDsRow.OrganizationIDs
}
groups := make([]codersdk.TemplateGroup, 0, len(dbGroups))
for _, group := range dbGroups {
var members []database.User
if group.Name == database.AllUsersGroup {
members, err = api.Database.GetAllOrganizationMembers(ctx, group.OrganizationID)
} else {
members, err = api.Database.GetGroupMembers(ctx, group.ID)
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
groups = append(groups, codersdk.TemplateGroup{
Group: convertGroup(group.Group, members),
Role: convertToTemplateRole(group.Actions),
})
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.TemplateACL{
Users: convertTemplateUsers(users, organizationIDsByUserID),
Groups: groups,
})
}
func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
template = httpmw.TemplateParam(r)
)
// Only users who are able to create templates (aka template admins)
// are able to control permissions.
if !api.Authorize(r, rbac.ActionCreate, template) {
httpapi.ResourceNotFound(rw)
return
}
var req codersdk.UpdateTemplateACL
if !httpapi.Read(ctx, rw, r, &req) {
return
}
validErrs := validateTemplateACLPerms(ctx, api.Database, req.UserPerms, "user_perms", true)
validErrs = append(validErrs,
validateTemplateACLPerms(ctx, api.Database, req.GroupPerms, "group_perms", false)...)
if len(validErrs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request to update template metadata!",
Validations: validErrs,
})
return
}
err := api.Database.InTx(func(tx database.Store) error {
if len(req.UserPerms) > 0 {
userACL := template.UserACL()
for id, role := range req.UserPerms {
// A user with an empty string implies
// deletion.
if role == "" {
delete(userACL, id)
continue
}
userACL[id] = convertSDKTemplateRole(role)
}
err := tx.UpdateTemplateUserACLByID(r.Context(), template.ID, userACL)
if err != nil {
return xerrors.Errorf("update template user ACL: %w", err)
}
}
if len(req.GroupPerms) > 0 {
groupACL := template.GroupACL()
for id, role := range req.GroupPerms {
// An id with an empty string implies
// deletion.
if role == "" {
delete(groupACL, id)
continue
}
groupACL[id] = convertSDKTemplateRole(role)
}
err := tx.UpdateTemplateGroupACLByID(ctx, template.ID, groupACL)
if err != nil {
return xerrors.Errorf("update template user ACL: %w", err)
}
}
return nil
})
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Successfully updated template ACL list.",
})
}
// nolint TODO fix stupid flag.
func validateTemplateACLPerms(ctx context.Context, db database.Store, perms map[string]codersdk.TemplateRole, field string, isUser bool) []codersdk.ValidationError {
var validErrs []codersdk.ValidationError
for k, v := range perms {
if err := validateTemplateRole(v); err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: err.Error()})
continue
}
id, err := uuid.Parse(k)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: "ID " + k + "must be a valid UUID."})
continue
}
if isUser {
// This could get slow if we get a ton of user perm updates.
_, err = db.GetUserByID(ctx, id)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: fmt.Sprintf("Failed to find resource with ID %q: %v", k, err.Error())})
continue
}
} else {
// This could get slow if we get a ton of group perm updates.
_, err = db.GetGroupByID(ctx, id)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: fmt.Sprintf("Failed to find resource with ID %q: %v", k, err.Error())})
continue
}
}
}
return validErrs
}
func convertTemplateUsers(tus []database.TemplateUser, orgIDsByUserIDs map[uuid.UUID][]uuid.UUID) []codersdk.TemplateUser {
users := make([]codersdk.TemplateUser, 0, len(tus))
for _, tu := range tus {
users = append(users, codersdk.TemplateUser{
User: convertUser(tu.User, orgIDsByUserIDs[tu.User.ID]),
Role: convertToTemplateRole(tu.Actions),
})
}
return users
}
func validateTemplateRole(role codersdk.TemplateRole) error {
actions := convertSDKTemplateRole(role)
if actions == nil && role != codersdk.TemplateRoleDeleted {
return xerrors.Errorf("role %q is not a valid Template role", role)
}
return nil
}
func convertToTemplateRole(actions []rbac.Action) codersdk.TemplateRole {
switch {
case len(actions) == 1 && actions[0] == rbac.ActionRead:
return codersdk.TemplateRoleView
case len(actions) == 1 && actions[0] == rbac.WildcardSymbol:
return codersdk.TemplateRoleAdmin
}
return ""
}
func convertSDKTemplateRole(role codersdk.TemplateRole) []rbac.Action {
switch role {
case codersdk.TemplateRoleAdmin:
return []rbac.Action{rbac.WildcardSymbol}
case codersdk.TemplateRoleView:
return []rbac.Action{rbac.ActionRead}
}
return nil
}
// TODO reduce the duplication across all of these.
func (api *API) rbacEnabledMW(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
api.entitlementsMu.RLock()
rbac := api.entitlements.Features[codersdk.FeatureRBAC].Enabled
api.entitlementsMu.RUnlock()
if !rbac {
httpapi.RouteNotFound(rw)
return
}
next.ServeHTTP(rw, r)
})
}

View File

@ -0,0 +1,707 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/testutil"
)
func TestTemplateACL(t *testing.T) {
t.Parallel()
t.Run("UserRoles", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleView,
user3.ID.String(): codersdk.TemplateRoleAdmin,
},
})
require.NoError(t, err)
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
templateUser2 := codersdk.TemplateUser{
User: user2,
Role: codersdk.TemplateRoleView,
}
templateUser3 := codersdk.TemplateUser{
User: user3,
Role: codersdk.TemplateRoleAdmin,
}
require.Len(t, acl.Users, 2)
require.Contains(t, acl.Users, templateUser2)
require.Contains(t, acl.Users, templateUser3)
})
t.Run("allUsersGroup", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
require.Len(t, acl.Groups[0].Members, 2)
require.Contains(t, acl.Groups[0].Members, user1)
require.Len(t, acl.Users, 0)
})
t.Run("NoGroups", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client1, _ := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
require.Len(t, acl.Users, 0)
// User should be able to read template due to allUsers group.
_, err = client1.Template(ctx, template.ID)
require.NoError(t, err)
allUsers := acl.Groups[0]
err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
GroupPerms: map[string]codersdk.TemplateRole{
allUsers.ID.String(): codersdk.TemplateRoleDeleted,
},
})
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 0)
require.Len(t, acl.Users, 0)
// User should not be able to read template due to allUsers group being deleted.
_, err = client1.Template(ctx, template.ID)
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})
// Test that we do not return deleted users.
t.Run("FilterDeletedUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user1.ID.String(): codersdk.TemplateRoleView,
},
})
require.NoError(t, err)
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Contains(t, acl.Users, codersdk.TemplateUser{
User: user1,
Role: codersdk.TemplateRoleView,
})
err = client.DeleteUser(ctx, user1.ID)
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Users, 0, "deleted users should be filtered")
})
// Test that we do not return suspended users.
t.Run("FilterSuspendedUsers", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user1.ID.String(): codersdk.TemplateRoleView,
},
})
require.NoError(t, err)
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Contains(t, acl.Users, codersdk.TemplateUser{
User: user1,
Role: codersdk.TemplateRoleView,
})
_, err = client.UpdateUserStatus(ctx, user1.ID.String(), codersdk.UserStatusSuspended)
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Users, 0, "suspended users should be filtered")
})
// Test that we do not return deleted groups.
t.Run("FilterDeletedGroups", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "test",
})
require.NoError(t, err)
err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
GroupPerms: map[string]codersdk.TemplateRole{
group.ID.String(): codersdk.TemplateRoleView,
},
})
require.NoError(t, err)
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
// Length should be 2 for test group and the implicit allUsers group.
require.Len(t, acl.Groups, 2)
require.Contains(t, acl.Groups, codersdk.TemplateGroup{
Group: group,
Role: codersdk.TemplateRoleView,
})
err = client.DeleteGroup(ctx, group.ID)
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
// Length should be 1 for the allUsers group.
require.Len(t, acl.Groups, 1)
require.NotContains(t, acl.Groups, codersdk.TemplateGroup{
Group: group,
Role: codersdk.TemplateRoleView,
})
})
t.Run("AdminCanPushVersions", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client1, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user1.ID.String(): codersdk.TemplateRoleView,
},
})
require.NoError(t, err)
data, err := echo.Tar(nil)
require.NoError(t, err)
file, err := client1.Upload(context.Background(), codersdk.ContentTypeTar, data)
require.NoError(t, err)
_, err = client1.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: "testme",
TemplateID: template.ID,
StorageSource: file.Hash,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeEcho,
})
require.Error(t, err)
err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user1.ID.String(): codersdk.TemplateRoleAdmin,
},
})
require.NoError(t, err)
_, err = client1.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: "testme",
TemplateID: template.ID,
StorageSource: file.Hash,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeEcho,
})
require.NoError(t, err)
})
}
func TestUpdateTemplateACL(t *testing.T) {
t.Parallel()
t.Run("UserPerms", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleView,
user3.ID.String(): codersdk.TemplateRoleAdmin,
},
})
require.NoError(t, err)
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
templateUser2 := codersdk.TemplateUser{
User: user2,
Role: codersdk.TemplateRoleView,
}
templateUser3 := codersdk.TemplateUser{
User: user3,
Role: codersdk.TemplateRoleAdmin,
}
require.Len(t, acl.Users, 2)
require.Contains(t, acl.Users, templateUser2)
require.Contains(t, acl.Users, templateUser3)
})
t.Run("DeleteUser", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleView,
user3.ID.String(): codersdk.TemplateRoleAdmin,
},
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.NoError(t, err)
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Contains(t, acl.Users, codersdk.TemplateUser{
User: user2,
Role: codersdk.TemplateRoleView,
})
require.Contains(t, acl.Users, codersdk.TemplateUser{
User: user3,
Role: codersdk.TemplateRoleAdmin,
})
req = codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleAdmin,
user3.ID.String(): codersdk.TemplateRoleDeleted,
},
}
err = client.UpdateTemplateACL(ctx, template.ID, req)
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Contains(t, acl.Users, codersdk.TemplateUser{
User: user2,
Role: codersdk.TemplateRoleAdmin,
})
require.NotContains(t, acl.Users, codersdk.TemplateUser{
User: user3,
Role: codersdk.TemplateRoleAdmin,
})
})
t.Run("InvalidUUID", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
"hi": "admin",
},
}
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.Error(t, err)
cerr, _ := codersdk.AsError(err)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
t.Run("InvalidUser", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
uuid.NewString(): "admin",
},
}
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.Error(t, err)
cerr, _ := codersdk.AsError(err)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
t.Run("InvalidRole", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
_, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): "updater",
},
}
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.Error(t, err)
cerr, _ := codersdk.AsError(err)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
t.Run("RegularUserCannotUpdatePerms", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client2, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleView,
},
}
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.NoError(t, err)
req = codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleAdmin,
},
}
err = client2.UpdateTemplateACL(ctx, template.ID, req)
require.Error(t, err)
cerr, _ := codersdk.AsError(err)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})
t.Run("RegularUserWithAdminCanUpdate", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client2, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user2.ID.String(): codersdk.TemplateRoleAdmin,
},
}
ctx, _ := testutil.Context(t)
err := client.UpdateTemplateACL(ctx, template.ID, req)
require.NoError(t, err)
req = codersdk.UpdateTemplateACL{
UserPerms: map[string]codersdk.TemplateRole{
user3.ID.String(): codersdk.TemplateRoleView,
},
}
err = client2.UpdateTemplateACL(ctx, template.ID, req)
require.NoError(t, err)
acl, err := client2.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Contains(t, acl.Users, codersdk.TemplateUser{
User: user3,
Role: codersdk.TemplateRoleView,
})
})
t.Run("allUsersGroup", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
require.Len(t, acl.Users, 0)
})
t.Run("CustomGroupHasAccess", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client1, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, _ := testutil.Context(t)
// Create a group to add to the template.
group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "test",
})
require.NoError(t, err)
// Check that the only current group is the allUsers group.
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
// Update the template to only allow access to the 'test' group.
err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
GroupPerms: map[string]codersdk.TemplateRole{
// The allUsers group shares the same ID as the organization.
user.OrganizationID.String(): codersdk.TemplateRoleDeleted,
group.ID.String(): codersdk.TemplateRoleView,
},
})
require.NoError(t, err)
// Get the ACL list for the template and assert the test group is
// present.
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
require.Len(t, acl.Users, 0)
require.Equal(t, group.ID, acl.Groups[0].ID)
// Try to get the template as the regular user. This should
// fail since we haven't been added to the template yet.
_, err = client1.Template(ctx, template.ID)
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
// Patch the group to add the regular user.
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user1.ID.String()},
})
require.NoError(t, err)
require.Len(t, group.Members, 1)
require.Equal(t, user1.ID, group.Members[0].ID)
// Fetching the template should succeed since our group has view access.
_, err = client1.Template(ctx, template.ID)
require.NoError(t, err)
})
t.Run("NoAccess", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
client1, _ := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
require.Len(t, acl.Users, 0)
// User should be able to read template due to allUsers group.
_, err = client1.Template(ctx, template.ID)
require.NoError(t, err)
allUsers := acl.Groups[0]
err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
GroupPerms: map[string]codersdk.TemplateRole{
allUsers.ID.String(): codersdk.TemplateRoleDeleted,
},
})
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 0)
require.Len(t, acl.Users, 0)
// User should not be able to read template due to allUsers group being deleted.
_, err = client1.Template(ctx, template.ID)
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})
}

View File

@ -0,0 +1,69 @@
package coderd_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/testutil"
)
func TestCreateWorkspace(t *testing.T) {
t.Parallel()
// Test that a user cannot indirectly access
// a template they do not have access to.
t.Run("Unauthorized", func(t *testing.T) {
t.Parallel()
client := coderdenttest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
RBACEnabled: true,
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
require.Len(t, acl.Users, 0)
err = client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
GroupPerms: map[string]codersdk.TemplateRole{
acl.Groups[0].ID.String(): codersdk.TemplateRoleDeleted,
},
})
require.NoError(t, err)
client1, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID)
_, err = client1.Template(ctx, template.ID)
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testme",
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
}
_, err = client1.CreateWorkspace(ctx, user.OrganizationID, user1.ID.String(), req)
require.Error(t, err)
})
}