mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
feat: add template RBAC/groups (#4235)
This commit is contained in:
111
enterprise/coderd/authorize_test.go
Normal file
111
enterprise/coderd/authorize_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
318
enterprise/coderd/groups.go
Normal 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,
|
||||
}
|
||||
}
|
504
enterprise/coderd/groups_test.go
Normal file
504
enterprise/coderd/groups_test.go
Normal 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())
|
||||
})
|
||||
}
|
@ -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 {
|
||||
|
@ -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),
|
||||
}),
|
||||
|
@ -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"])
|
||||
})
|
||||
}
|
||||
|
262
enterprise/coderd/templates.go
Normal file
262
enterprise/coderd/templates.go
Normal 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)
|
||||
})
|
||||
}
|
707
enterprise/coderd/templates_test.go
Normal file
707
enterprise/coderd/templates_test.go
Normal 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())
|
||||
})
|
||||
}
|
69
enterprise/coderd/workspaces_test.go
Normal file
69
enterprise/coderd/workspaces_test.go
Normal 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)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user