From 3ca152a8dc8c37151597ca1141d48b8816f38b6b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 22 May 2024 18:30:02 -1000 Subject: [PATCH 01/14] chore: implement patching custom organization roles --- cli/cliui/parameter.go | 5 +- cli/cliui/select.go | 22 ++++++-- cli/cliui/select_test.go | 5 +- coderd/coderd.go | 6 +- coderd/roles.go | 54 ++++++++++++++++++ enterprise/coderd/coderd.go | 5 ++ enterprise/coderd/roles.go | 97 ++++++++++++++++++++++++++++++++- enterprise/coderd/roles_test.go | 11 +++- scripts/rbacgen/codersdk.gotmpl | 12 ++++ 9 files changed, 205 insertions(+), 12 deletions(-) diff --git a/cli/cliui/parameter.go b/cli/cliui/parameter.go index 897ddec4de4d6..8080ef1a96906 100644 --- a/cli/cliui/parameter.go +++ b/cli/cliui/parameter.go @@ -43,7 +43,10 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te return "", err } - values, err := MultiSelect(inv, options) + values, err := MultiSelect(inv, MultiSelectOptions{ + Options: options, + Defaults: options, + }) if err == nil { v, err := json.Marshal(&values) if err != nil { diff --git a/cli/cliui/select.go b/cli/cliui/select.go index 3ae27ee811e50..8a56ae93797ca 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -21,7 +21,7 @@ func init() { {{- .CurrentOpt.Value}} {{- color "reset"}} {{end}} - +{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }} {{- if not .ShowAnswer }} {{- if .Config.Icons.Help.Text }} {{- if .FilterMessage }}{{ "Search:" }}{{ .FilterMessage }} @@ -44,18 +44,20 @@ func init() { {{- " "}}{{- .CurrentOpt.Value}} {{end}} {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} +{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }} {{- if not .ShowAnswer }} {{- "\n"}} {{- range $ix, $option := .PageEntries}} {{- template "option" $.IterateOption $ix $option}} {{- end}} -{{- end}}` +{{- end }}` } type SelectOptions struct { Options []string // Default will be highlighted first if it's a valid option. Default string + Message string Size int HideSearch bool } @@ -122,6 +124,7 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) { Options: opts.Options, Default: defaultOption, PageSize: opts.Size, + Message: opts.Message, }, &value, survey.WithIcons(func(is *survey.IconSet) { is.Help.Text = "Type to search" if opts.HideSearch { @@ -138,15 +141,22 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) { return value, err } -func MultiSelect(inv *serpent.Invocation, items []string) ([]string, error) { +type MultiSelectOptions struct { + Message string + Options []string + Defaults []string +} + +func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) { // Similar hack is applied to Select() if flag.Lookup("test.v") != nil { - return items, nil + return opts.Defaults, nil } prompt := &survey.MultiSelect{ - Options: items, - Default: items, + Message: opts.Message, + Options: opts.Options, + Default: opts.Defaults, } var values []string diff --git a/cli/cliui/select_test.go b/cli/cliui/select_test.go index c399121adb6ec..c0da49714fc40 100644 --- a/cli/cliui/select_test.go +++ b/cli/cliui/select_test.go @@ -107,7 +107,10 @@ func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) { var values []string cmd := &serpent.Command{ Handler: func(inv *serpent.Invocation) error { - selectedItems, err := cliui.MultiSelect(inv, items) + selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{ + Options: items, + Defaults: items, + }) if err == nil { values = selectedItems } diff --git a/coderd/coderd.go b/coderd/coderd.go index 9ee21a23cf79f..ad874d672822d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -812,8 +812,6 @@ func New(options *Options) *API { httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.organization) - r.Patch("/", api.patchOrganization) - r.Delete("/", api.deleteOrganization) r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) @@ -829,6 +827,8 @@ func New(options *Options) *API { }) r.Route("/members", func(r chi.Router) { r.Get("/roles", api.assignableOrgRoles) + r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)). + Patch("/roles", api.patchOrgRoles) r.Route("/{user}", func(r chi.Router) { r.Use( httpmw.ExtractOrganizationMemberParam(options.Database), @@ -1249,6 +1249,8 @@ type API struct { // passed to dbauthz. AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] PortSharer atomic.Pointer[portsharing.PortSharer] + // CustomRoleHandler is the AGPL/Enterprise implementation for custom roles. + CustomRoleHandler atomic.Pointer[CustomRoleHandler] HTTPAuth *HTTPAuthorizer diff --git a/coderd/roles.go b/coderd/roles.go index a00af23ce98eb..c0caf7bb0be9f 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -1,6 +1,7 @@ package coderd import ( + "context" "net/http" "github.com/google/uuid" @@ -16,6 +17,59 @@ import ( "github.com/coder/coder/v2/coderd/rbac" ) +// CustomRoleHandler handles AGPL/Enterprise interface for handling custom +// roles. Ideally only included in the enterprise package, but the routes are +// intermixed with AGPL endpoints. +type CustomRoleHandler interface { + PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) +} + +type agplCustomRoleHandler struct{} + +func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) { + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + Message: "Creating and updating custom roles is an Enterprise feature. Contact sales!", + }) + return codersdk.Role{}, false +} + +// patchRole will allow creating a custom organization role +// +// @Summary Upsert a custom organization role +// @ID upsert-a-custom-organization-role +// @Security CoderSessionToken +// @Produce json +// @Tags Members +// @Success 200 {array} codersdk.Role +// @Router /organizations/{organization}/members/roles [patch] +func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + handler = *api.CustomRoleHandler.Load() + organization = httpmw.OrganizationParam(r) + ) + + var req codersdk.Role + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + if err := httpapi.NameValid(req.Name); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid role name", + Detail: err.Error(), + }) + return + } + + updated, ok := handler.PatchOrganizationRole(ctx, api.Database, rw, organization.ID, req) + if !ok { + return + } + + httpapi.Write(ctx, rw, http.StatusOK, updated) +} + // AssignableSiteRoles returns all site wide roles that can be assigned. // // @Summary Get site member roles diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 524bfd26f3d74..144a6edc3f66a 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -761,6 +761,11 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.AGPL.PortSharer.Store(&ps) } + if initial, changed, enabled := featureChanged(codersdk.FeatureCustomRoles); shouldUpdate(initial, changed, enabled) { + var handler coderd.CustomRoleHandler = &enterpriseCustomRoleHandler{Enabled: enabled} + api.AGPL.CustomRoleHandler.Store(&handler) + } + // External token encryption is soft-enforced featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption] featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0 diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index 8e0827c9b3b02..85f2ecbdd0032 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -1,6 +1,8 @@ package coderd import ( + "context" + "fmt" "net/http" "github.com/google/uuid" @@ -12,6 +14,100 @@ import ( "github.com/coder/coder/v2/codersdk" ) +type enterpriseCustomRoleHandler struct { + Enabled bool +} + +func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) { + if !h.Enabled { + httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ + Message: "Custom roles is not enabled", + }) + return codersdk.Role{}, false + } + + // Only organization permissions are allowed to be granted + if len(role.SitePermissions) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request, not allowed to assign site wide permissions for an organization role.", + Detail: "organization scoped roles may not contain site wide permissions", + }) + return codersdk.Role{}, false + } + + if len(role.UserPermissions) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request, not allowed to assign user permissions for an organization role.", + Detail: "organization scoped roles may not contain user permissions", + }) + return codersdk.Role{}, false + } + + if len(role.OrganizationPermissions) > 1 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request, Only 1 organization can be assigned permissions", + Detail: "roles can only contain 1 organization", + }) + return codersdk.Role{}, false + } + + if len(role.OrganizationPermissions) == 1 { + _, exists := role.OrganizationPermissions[orgID.String()] + if !exists { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Invalid request, expected permissions for only the organization %q", orgID.String()), + Detail: fmt.Sprintf("only org id %s allowed", orgID.String()), + }) + return codersdk.Role{}, false + } + } + + // Make sure all permissions inputted are valid according to our policy. + rbacRole := db2sdk.RoleToRBAC(role) + args, err := rolestore.ConvertRoleToDB(rbacRole) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid request", + Detail: err.Error(), + }) + return codersdk.Role{}, false + } + + inserted, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ + Name: args.Name, + DisplayName: args.DisplayName, + OrganizationID: uuid.NullUUID{ + UUID: orgID, + Valid: true, + }, + SitePermissions: args.SitePermissions, + OrgPermissions: args.OrgPermissions, + UserPermissions: args.UserPermissions, + }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return codersdk.Role{}, false + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Failed to update role permissions", + Detail: err.Error(), + }) + return codersdk.Role{}, false + } + + convertedInsert, err := rolestore.ConvertDBRole(inserted) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Permissions were updated, unable to read them back out of the database.", + Detail: err.Error(), + }) + return codersdk.Role{}, false + } + + return db2sdk.Role(convertedInsert), true +} + // patchRole will allow creating a custom role // // @Summary Upsert a custom site-wide role @@ -61,7 +157,6 @@ func (api *API) patchRole(rw http.ResponseWriter, r *http.Request) { inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ Name: args.Name, DisplayName: args.DisplayName, - OrganizationID: uuid.NullUUID{}, SitePermissions: args.SitePermissions, OrgPermissions: args.OrgPermissions, UserPermissions: args.UserPermissions, diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index a7db9b718d946..6cf55e29b4047 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -67,9 +67,18 @@ func TestCustomRole(t *testing.T) { allRoles, err := tmplAdmin.ListSiteRoles(ctx) require.NoError(t, err) + var foundRole codersdk.AssignableRoles require.True(t, slices.ContainsFunc(allRoles, func(selected codersdk.AssignableRoles) bool { - return selected.Name == role.Name + if selected.Name == role.Name { + foundRole = selected + return true + } + return false }), "role missing from site role list") + + require.Len(t, foundRole.SitePermissions, 7) + require.Len(t, foundRole.OrganizationPermissions, 0) + require.Len(t, foundRole.UserPermissions, 0) }) // Revoked licenses cannot modify/create custom roles, but they can diff --git a/scripts/rbacgen/codersdk.gotmpl b/scripts/rbacgen/codersdk.gotmpl index 1492eaf86c2bf..dff4e165b1df5 100644 --- a/scripts/rbacgen/codersdk.gotmpl +++ b/scripts/rbacgen/codersdk.gotmpl @@ -16,3 +16,15 @@ const ( {{ $element.Enum }} RBACAction = "{{ $element.Value }}" {{- end }} ) + +// RBACResourceActions is the mapping of resources to which actions are valid for +// said resource type. +var RBACResourceActions = map[RBACResource][]RBACAction{ + {{- range $element := . }} + Resource{{ pascalCaseName $element.FunctionName }}: []RBACAction{ + {{- range $actionValue, $_ := $element.Actions }} + {{- actionEnum $actionValue -}}, + {{- end -}} + }, + {{- end }} +} From d231cdf02de606c41f9a6d8335db04e0b89ba459 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 22 May 2024 19:24:02 -1000 Subject: [PATCH 02/14] fixup test to assign org role instead of site --- coderd/coderd.go | 9 ++- coderd/database/dbauthz/dbauthz.go | 13 ++++- coderd/members.go | 37 +----------- codersdk/roles.go | 18 +++++- enterprise/coderd/coderd.go | 16 ----- enterprise/coderd/roles.go | 77 ------------------------ enterprise/coderd/roles_test.go | 94 +++++++++++++++--------------- 7 files changed, 81 insertions(+), 183 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index ad874d672822d..76aa82faaf2d3 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -826,9 +826,12 @@ func New(options *Options) *API { }) }) r.Route("/members", func(r chi.Router) { - r.Get("/roles", api.assignableOrgRoles) - r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)). - Patch("/roles", api.patchOrgRoles) + r.Route("/roles", func(r chi.Router) { + r.Get("/", api.assignableOrgRoles) + r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)). + Patch("/", api.patchOrgRoles) + }) + r.Route("/{user}", func(r chi.Router) { r.Use( httpmw.ExtractOrganizationMemberParam(options.Database), diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 0ab78e75fe196..1d611eed9d573 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -600,7 +600,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r customRoles := make([]string, 0) // Validate that the roles being assigned are valid. for _, r := range grantedRoles { - _, isOrgRole := rbac.IsOrgRole(r) + roleOrgIDStr, isOrgRole := rbac.IsOrgRole(r) if shouldBeOrgRoles && !isOrgRole { return xerrors.Errorf("Must only update org roles") } @@ -608,6 +608,17 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r return xerrors.Errorf("Must only update site wide roles") } + if shouldBeOrgRoles { + roleOrgID, err := uuid.Parse(roleOrgIDStr) + if err != nil { + return xerrors.Errorf("role %q has invalid uuid for org: %w", r, err) + } + + if roleOrgID != *orgID { + return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, (*orgID).String()) + } + } + // All roles should be valid roles if _, err := rbac.RoleByName(r); err != nil { customRoles = append(customRoles, r) diff --git a/coderd/members.go b/coderd/members.go index 6a3fe3b2bcb09..beae302ab3124 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -1,13 +1,8 @@ package coderd import ( - "context" "net/http" - "github.com/google/uuid" - - "golang.org/x/xerrors" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/rbac" @@ -48,7 +43,7 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { return } - updatedUser, err := api.updateOrganizationMemberRoles(ctx, database.UpdateMemberRolesParams{ + updatedUser, err := api.Database.UpdateMemberRoles(ctx, database.UpdateMemberRolesParams{ GrantedRoles: params.Roles, UserID: member.UserID, OrgID: organization.ID, @@ -63,36 +58,6 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertOrganizationMember(updatedUser)) } -func (api *API) updateOrganizationMemberRoles(ctx context.Context, args database.UpdateMemberRolesParams) (database.OrganizationMember, error) { - // Enforce only site wide roles - for _, r := range args.GrantedRoles { - // Must be an org role for the org in the args - orgID, ok := rbac.IsOrgRole(r) - if !ok { - return database.OrganizationMember{}, xerrors.Errorf("must only update organization roles") - } - - roleOrg, err := uuid.Parse(orgID) - if err != nil { - return database.OrganizationMember{}, xerrors.Errorf("Role must have proper UUIDs for organization, %q does not", r) - } - - if roleOrg != args.OrgID { - return database.OrganizationMember{}, xerrors.Errorf("Must only pass roles for org %q", args.OrgID.String()) - } - - if _, err := rbac.RoleByName(r); err != nil { - return database.OrganizationMember{}, xerrors.Errorf("%q is not a supported organization role", r) - } - } - - updatedUser, err := api.Database.UpdateMemberRoles(ctx, args) - if err != nil { - return database.OrganizationMember{}, xerrors.Errorf("Update site roles: %w", err) - } - return updatedUser, nil -} - func convertOrganizationMember(mem database.OrganizationMember) codersdk.OrganizationMember { convertedMember := codersdk.OrganizationMember{ UserID: mem.UserID, diff --git a/codersdk/roles.go b/codersdk/roles.go index c803e92f44bb2..2fbe304de25c2 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -44,9 +44,21 @@ type Role struct { UserPermissions []Permission `json:"user_permissions" table:"user_permissions"` } -// PatchRole will upsert a custom site wide role -func (c *Client) PatchRole(ctx context.Context, req Role) (Role, error) { - res, err := c.Request(ctx, http.MethodPatch, "/api/v2/users/roles", req) +// FullName returns the role name scoped to the organization ID. This is useful if +// printing a set of roles from different scopes, as duplicated names across multiple +// scopes will become unique. +// In practice, this is primarily used in testing. +func (r Role) FullName() string { + if r.OrganizationID == "" { + return r.Name + } + return r.Name + ":" + r.OrganizationID +} + +// PatchOrganizationRole will upsert a custom organization role +func (c *Client) PatchOrganizationRole(ctx context.Context, organizationID uuid.UUID, req Role) (Role, error) { + res, err := c.Request(ctx, http.MethodPatch, + fmt.Sprintf("/api/v2/organizations/%s/members/roles", organizationID.String()), req) if err != nil { return Role{}, err } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 144a6edc3f66a..574d2c12dd2de 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -327,22 +327,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { }) }) - r.Route("/users/roles", func(r chi.Router) { - r.Use( - apiKeyMiddleware, - ) - r.Group(func(r chi.Router) { - r.Use( - api.customRolesEnabledMW, - ) - r.Patch("/", api.patchRole) - }) - // Unfortunate, but this r.Route overrides the AGPL roles route. - // The AGPL does not have the entitlements to block the licensed - // routes, so we need to duplicate the AGPL here. - r.Get("/", api.AGPL.AssignableSiteRoles) - }) - r.Route("/users/{user}/quiet-hours", func(r chi.Router) { r.Use( api.autostopRequirementEnabledMW, diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index 85f2ecbdd0032..3e76df8001ba7 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -107,80 +107,3 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, return db2sdk.Role(convertedInsert), true } - -// patchRole will allow creating a custom role -// -// @Summary Upsert a custom site-wide role -// @ID upsert-a-custom-site-wide-role -// @Security CoderSessionToken -// @Produce json -// @Tags Members -// @Success 200 {array} codersdk.Role -// @Router /users/roles [patch] -func (api *API) patchRole(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var req codersdk.Role - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - if err := httpapi.NameValid(req.Name); err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid role name", - Detail: err.Error(), - }) - return - } - - if len(req.OrganizationPermissions) > 0 { - // Org perms should be assigned only in org specific roles. Otherwise, - // it gets complicated to keep track of who can do what. - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid request, not allowed to assign organization permissions for a site wide role.", - Detail: "site wide roles may not contain organization specific permissions", - }) - return - } - - // Make sure all permissions inputted are valid according to our policy. - rbacRole := db2sdk.RoleToRBAC(req) - args, err := rolestore.ConvertRoleToDB(rbacRole) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid request", - Detail: err.Error(), - }) - return - } - - inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ - Name: args.Name, - DisplayName: args.DisplayName, - SitePermissions: args.SitePermissions, - OrgPermissions: args.OrgPermissions, - UserPermissions: args.UserPermissions, - }) - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to update role permissions", - Detail: err.Error(), - }) - return - } - - convertedInsert, err := rolestore.ConvertDBRole(inserted) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Permissions were updated, unable to read them back out of the database.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(convertedInsert)) -} diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 6cf55e29b4047..b4c0ee63024cd 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -5,10 +5,10 @@ import ( "slices" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "github.com/coder/coder/v2/enterprise/coderd/license" @@ -16,19 +16,23 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestCustomRole(t *testing.T) { +func TestCustomOrganizationRole(t *testing.T) { t.Parallel() - templateAdminCustom := codersdk.Role{ - Name: "test-role", - DisplayName: "Testing Purposes", - // Basically creating a template admin manually - SitePermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights}, - codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead}, - codersdk.ResourceWorkspace: {codersdk.ActionRead}, - }), - OrganizationPermissions: nil, - UserPermissions: nil, + templateAdminCustom := func(orgID uuid.UUID) codersdk.Role { + return codersdk.Role{ + Name: "test-role", + DisplayName: "Testing Purposes", + // Basically creating a template admin manually + SitePermissions: nil, + OrganizationPermissions: map[string][]codersdk.Permission{ + orgID.String(): codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights}, + codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead}, + codersdk.ResourceWorkspace: {codersdk.ActionRead}, + }), + }, + UserPermissions: nil, + } } // Create, assign, and use a custom role @@ -50,21 +54,24 @@ func TestCustomRole(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) //nolint:gocritic // owner is required for this - role, err := owner.PatchRole(ctx, templateAdminCustom) + role, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID)) require.NoError(t, err, "upsert role") // Assign the custom template admin role - tmplAdmin, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name) + tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName()) // Assert the role exists - roleNamesF := func(role codersdk.SlimRole) string { return role.Name } - require.Contains(t, db2sdk.List(user.Roles, roleNamesF), role.Name) + // TODO: At present user roles are not returned by the user endpoints. + // Changing this might mess up the UI in how it renders the roles on the + // users page. When the users endpoint is updated, this should be uncommented. + //roleNamesF := func(role codersdk.SlimRole) string { return role.Name } + //require.Contains(t, db2sdk.List(user.Roles, roleNamesF), role.Name) // Try to create a template version coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil) // Verify the role exists in the list - allRoles, err := tmplAdmin.ListSiteRoles(ctx) + allRoles, err := tmplAdmin.ListOrganizationRoles(ctx, first.OrganizationID) require.NoError(t, err) var foundRole codersdk.AssignableRoles @@ -74,10 +81,10 @@ func TestCustomRole(t *testing.T) { return true } return false - }), "role missing from site role list") + }), "role missing from org role list") - require.Len(t, foundRole.SitePermissions, 7) - require.Len(t, foundRole.OrganizationPermissions, 0) + require.Len(t, foundRole.SitePermissions, 0) + require.Len(t, foundRole.OrganizationPermissions[first.OrganizationID.String()], 7) require.Len(t, foundRole.UserPermissions, 0) }) @@ -101,7 +108,7 @@ func TestCustomRole(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) //nolint:gocritic // owner is required for this - role, err := owner.PatchRole(ctx, templateAdminCustom) + role, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID)) require.NoError(t, err, "upsert role") // Remove the license to block enterprise functionality @@ -114,11 +121,11 @@ func TestCustomRole(t *testing.T) { } // Verify functionality is lost - _, err = owner.PatchRole(ctx, templateAdminCustom) - require.ErrorContains(t, err, "Custom roles is an Enterprise feature", "upsert role") + _, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID)) + require.ErrorContains(t, err, "roles is not enabled") // Assign the custom template admin role - tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name) + tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName()) // Try to create a template version, eg using the custom role coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil) @@ -142,26 +149,24 @@ func TestCustomRole(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) //nolint:gocritic // owner is required for this - role, err := owner.PatchRole(ctx, templateAdminCustom) + role, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID)) require.NoError(t, err, "upsert role") // Assign the custom template admin role - tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name) + tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName()) // Try to create a template version, eg using the custom role coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil) //nolint:gocritic // owner is required for this - role, err = owner.PatchRole(ctx, codersdk.Role{ - Name: templateAdminCustom.Name, - DisplayName: templateAdminCustom.DisplayName, - // These are all left nil, which sets the custom role to have 0 - // permissions. Omitting this does not "inherit" what already - // exists. - SitePermissions: nil, - OrganizationPermissions: nil, - UserPermissions: nil, - }) + newRole := templateAdminCustom(first.OrganizationID) + // These are all left nil, which sets the custom role to have 0 + // permissions. Omitting this does not "inherit" what already + // exists. + newRole.SitePermissions = nil + newRole.OrganizationPermissions = nil + newRole.UserPermissions = nil + role, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, newRole) require.NoError(t, err, "upsert role with override") // The role should no longer have template perms @@ -181,7 +186,7 @@ func TestCustomRole(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} - owner, _ := coderdenttest.New(t, &coderdenttest.Options{ + owner, first := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, }, @@ -195,15 +200,10 @@ func TestCustomRole(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) //nolint:gocritic // owner is required for this - _, err := owner.PatchRole(ctx, codersdk.Role{ - Name: "Bad_Name", // No underscores allowed - DisplayName: "Testing Purposes", - // Basically creating a template admin manually - SitePermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights}, - codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead}, - codersdk.ResourceWorkspace: {codersdk.ActionRead}, - }), + _, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, codersdk.Role{ + Name: "Bad_Name", // No underscores allowed + DisplayName: "Testing Purposes", + SitePermissions: nil, OrganizationPermissions: nil, UserPermissions: nil, }) From 5c80ea08e2dcb72fd5e7a8bc1a1f89bb5f5e2617 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 24 May 2024 07:46:43 -1000 Subject: [PATCH 03/14] cleanup and make gen --- coderd/apidoc/docs.go | 52 +++++----- coderd/apidoc/swagger.json | 44 ++++----- coderd/coderd.go | 3 + coderd/database/dbauthz/dbauthz.go | 2 +- coderd/roles.go | 2 +- docs/api/members.md | 150 ++++++++++++++--------------- enterprise/coderd/users.go | 25 ----- 7 files changed, 128 insertions(+), 150 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 37e121e483068..1cb08303ba25b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2225,6 +2225,32 @@ const docTemplate = `{ } } } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Members" + ], + "summary": "Upsert a custom organization role", + "operationId": "upsert-a-custom-organization-role", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Role" + } + } + } + } } }, "/organizations/{organization}/members/{user}/roles": { @@ -4362,32 +4388,6 @@ const docTemplate = `{ } } } - }, - "patch": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Members" - ], - "summary": "Upsert a custom site-wide role", - "operationId": "upsert-a-custom-site-wide-role", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Role" - } - } - } - } } }, "/users/{user}": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 293e9e8e65265..3f2eb4c33df3e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1942,6 +1942,28 @@ } } } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Members"], + "summary": "Upsert a custom organization role", + "operationId": "upsert-a-custom-organization-role", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Role" + } + } + } + } } }, "/organizations/{organization}/members/{user}/roles": { @@ -3841,28 +3863,6 @@ } } } - }, - "patch": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Members"], - "summary": "Upsert a custom site-wide role", - "operationId": "upsert-a-custom-site-wide-role", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Role" - } - } - } - } } }, "/users/{user}": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 76aa82faaf2d3..734eff69e3864 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -424,6 +424,7 @@ func New(options *Options) *API { TemplateScheduleStore: options.TemplateScheduleStore, UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore, AccessControlStore: options.AccessControlStore, + CustomRoleHandler: atomic.Pointer[CustomRoleHandler]{}, Experiments: experiments, healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{}, Acquirer: provisionerdserver.NewAcquirer( @@ -436,6 +437,8 @@ func New(options *Options) *API { workspaceUsageTracker: options.WorkspaceUsageTracker, } + var customRoleHandler CustomRoleHandler = &agplCustomRoleHandler{} + api.CustomRoleHandler.Store(&customRoleHandler) api.AppearanceFetcher.Store(&appearance.DefaultFetcher) api.PortSharer.Store(&portsharing.DefaultPortSharer) buildInfo := codersdk.BuildInfoResponse{ diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1d611eed9d573..1c0b728f4d843 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -615,7 +615,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r } if roleOrgID != *orgID { - return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, (*orgID).String()) + return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, orgID.String()) } } diff --git a/coderd/roles.go b/coderd/roles.go index c0caf7bb0be9f..a85c0a354f602 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -26,7 +26,7 @@ type CustomRoleHandler interface { type agplCustomRoleHandler struct{} -func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) { +func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, _ database.Store, rw http.ResponseWriter, _ uuid.UUID, _ codersdk.Role) (codersdk.Role, bool) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Creating and updating custom roles is an Enterprise feature. Contact sales!", }) diff --git a/docs/api/members.md b/docs/api/members.md index 27536a6c836fa..cd3b139a3e9b3 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -136,75 +136,18 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Assign role to organization member +## Upsert a custom organization role ### Code samples ```shell # Example request using curl -curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members/{user}/roles \ - -H 'Content-Type: application/json' \ +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/members/roles \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`PUT /organizations/{organization}/members/{user}/roles` - -> Body parameter - -```json -{ - "roles": ["string"] -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -| -------------- | ---- | ------------------------------------------------------ | -------- | -------------------- | -| `organization` | path | string | true | Organization ID | -| `user` | path | string | true | User ID, name, or me | -| `body` | body | [codersdk.UpdateRoles](schemas.md#codersdkupdateroles) | true | Update roles request | - -### Example responses - -> 200 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "roles": [ - { - "display_name": "string", - "name": "string" - } - ], - "updated_at": "2019-08-24T14:15:22Z", - "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationMember](schemas.md#codersdkorganizationmember) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Get site member roles - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/users/roles \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /users/roles` +`PATCH /organizations/{organization}/members/roles` ### Example responses @@ -213,8 +156,6 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \ ```json [ { - "assignable": true, - "built_in": true, "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -254,19 +195,17 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.AssignableRoles](schemas.md#codersdkassignableroles) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Role](schemas.md#codersdkrole) | -

Response Schema

+

Response Schema

Status Code **200** | Name | Type | Required | Restrictions | Description | | ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- | | `[array item]` | array | false | | | -| `» assignable` | boolean | false | | | -| `» built_in` | boolean | false | | Built in roles are immutable | | `» display_name` | string | false | | | | `» name` | string | false | | | | `» organization_id` | string(uuid) | false | | | @@ -323,18 +262,75 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Upsert a custom site-wide role +## Assign role to organization member ### Code samples ```shell # Example request using curl -curl -X PATCH http://coder-server:8080/api/v2/users/roles \ +curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members/{user}/roles \ + -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`PATCH /users/roles` +`PUT /organizations/{organization}/members/{user}/roles` + +> Body parameter + +```json +{ + "roles": ["string"] +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------------------------------------------------------ | -------- | -------------------- | +| `organization` | path | string | true | Organization ID | +| `user` | path | string | true | User ID, name, or me | +| `body` | body | [codersdk.UpdateRoles](schemas.md#codersdkupdateroles) | true | Update roles request | + +### Example responses + +> 200 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "roles": [ + { + "display_name": "string", + "name": "string" + } + ], + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OrganizationMember](schemas.md#codersdkorganizationmember) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Get site member roles + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/users/roles \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /users/roles` ### Example responses @@ -343,6 +339,8 @@ curl -X PATCH http://coder-server:8080/api/v2/users/roles \ ```json [ { + "assignable": true, + "built_in": true, "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", @@ -382,17 +380,19 @@ curl -X PATCH http://coder-server:8080/api/v2/users/roles \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Role](schemas.md#codersdkrole) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.AssignableRoles](schemas.md#codersdkassignableroles) | -

Response Schema

+

Response Schema

Status Code **200** | Name | Type | Required | Restrictions | Description | | ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- | | `[array item]` | array | false | | | +| `» assignable` | boolean | false | | | +| `» built_in` | boolean | false | | Built in roles are immutable | | `» display_name` | string | false | | | | `» name` | string | false | | | | `» organization_id` | string(uuid) | false | | | diff --git a/enterprise/coderd/users.go b/enterprise/coderd/users.go index a29aa1836557d..935eeb8f6e689 100644 --- a/enterprise/coderd/users.go +++ b/enterprise/coderd/users.go @@ -14,31 +14,6 @@ import ( "github.com/coder/coder/v2/codersdk" ) -func (api *API) customRolesEnabledMW(next http.Handler) http.Handler { - return httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentCustomRoles)( - http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - // Entitlement must be enabled. - api.entitlementsMu.RLock() - entitled := api.entitlements.Features[codersdk.FeatureCustomRoles].Entitlement != codersdk.EntitlementNotEntitled - enabled := api.entitlements.Features[codersdk.FeatureCustomRoles].Enabled - api.entitlementsMu.RUnlock() - if !entitled { - httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ - Message: "Custom roles is an Enterprise feature. Contact sales!", - }) - return - } - if !enabled { - httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ - Message: "Custom roles is not enabled", - }) - return - } - - next.ServeHTTP(rw, r) - })) -} - func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Entitlement must be enabled. From b55d1c5fb7496b271b7a3247e6bf9da1562a08e1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 24 May 2024 08:37:01 -1000 Subject: [PATCH 04/14] Remove diffs for the cli, moving to another branch --- cli/cliui/parameter.go | 5 +---- cli/cliui/select.go | 22 ++++++---------------- cli/cliui/select_test.go | 5 +---- enterprise/coderd/roles_test.go | 2 +- scripts/rbacgen/codersdk.gotmpl | 12 ------------ 5 files changed, 9 insertions(+), 37 deletions(-) diff --git a/cli/cliui/parameter.go b/cli/cliui/parameter.go index 8080ef1a96906..897ddec4de4d6 100644 --- a/cli/cliui/parameter.go +++ b/cli/cliui/parameter.go @@ -43,10 +43,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te return "", err } - values, err := MultiSelect(inv, MultiSelectOptions{ - Options: options, - Defaults: options, - }) + values, err := MultiSelect(inv, options) if err == nil { v, err := json.Marshal(&values) if err != nil { diff --git a/cli/cliui/select.go b/cli/cliui/select.go index 8a56ae93797ca..3ae27ee811e50 100644 --- a/cli/cliui/select.go +++ b/cli/cliui/select.go @@ -21,7 +21,7 @@ func init() { {{- .CurrentOpt.Value}} {{- color "reset"}} {{end}} -{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }} + {{- if not .ShowAnswer }} {{- if .Config.Icons.Help.Text }} {{- if .FilterMessage }}{{ "Search:" }}{{ .FilterMessage }} @@ -44,20 +44,18 @@ func init() { {{- " "}}{{- .CurrentOpt.Value}} {{end}} {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }} {{- if not .ShowAnswer }} {{- "\n"}} {{- range $ix, $option := .PageEntries}} {{- template "option" $.IterateOption $ix $option}} {{- end}} -{{- end }}` +{{- end}}` } type SelectOptions struct { Options []string // Default will be highlighted first if it's a valid option. Default string - Message string Size int HideSearch bool } @@ -124,7 +122,6 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) { Options: opts.Options, Default: defaultOption, PageSize: opts.Size, - Message: opts.Message, }, &value, survey.WithIcons(func(is *survey.IconSet) { is.Help.Text = "Type to search" if opts.HideSearch { @@ -141,22 +138,15 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) { return value, err } -type MultiSelectOptions struct { - Message string - Options []string - Defaults []string -} - -func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) { +func MultiSelect(inv *serpent.Invocation, items []string) ([]string, error) { // Similar hack is applied to Select() if flag.Lookup("test.v") != nil { - return opts.Defaults, nil + return items, nil } prompt := &survey.MultiSelect{ - Message: opts.Message, - Options: opts.Options, - Default: opts.Defaults, + Options: items, + Default: items, } var values []string diff --git a/cli/cliui/select_test.go b/cli/cliui/select_test.go index c0da49714fc40..c399121adb6ec 100644 --- a/cli/cliui/select_test.go +++ b/cli/cliui/select_test.go @@ -107,10 +107,7 @@ func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) { var values []string cmd := &serpent.Command{ Handler: func(inv *serpent.Invocation) error { - selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{ - Options: items, - Defaults: items, - }) + selectedItems, err := cliui.MultiSelect(inv, items) if err == nil { values = selectedItems } diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index b4c0ee63024cd..a3fe444d30135 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -166,7 +166,7 @@ func TestCustomOrganizationRole(t *testing.T) { newRole.SitePermissions = nil newRole.OrganizationPermissions = nil newRole.UserPermissions = nil - role, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, newRole) + _, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, newRole) require.NoError(t, err, "upsert role with override") // The role should no longer have template perms diff --git a/scripts/rbacgen/codersdk.gotmpl b/scripts/rbacgen/codersdk.gotmpl index dff4e165b1df5..1492eaf86c2bf 100644 --- a/scripts/rbacgen/codersdk.gotmpl +++ b/scripts/rbacgen/codersdk.gotmpl @@ -16,15 +16,3 @@ const ( {{ $element.Enum }} RBACAction = "{{ $element.Value }}" {{- end }} ) - -// RBACResourceActions is the mapping of resources to which actions are valid for -// said resource type. -var RBACResourceActions = map[RBACResource][]RBACAction{ - {{- range $element := . }} - Resource{{ pascalCaseName $element.FunctionName }}: []RBACAction{ - {{- range $actionValue, $_ := $element.Actions }} - {{- actionEnum $actionValue -}}, - {{- end -}} - }, - {{- end }} -} From fb1dddc5c062a48e9fe1e5e01ba7b8f43119d170 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 24 May 2024 08:45:29 -1000 Subject: [PATCH 05/14] revert route removal --- coderd/coderd.go | 2 ++ enterprise/coderd/roles_test.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 734eff69e3864..9c748d06eeb71 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -815,6 +815,8 @@ func New(options *Options) *API { httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.organization) + r.Patch("/", api.patchOrganization) + r.Delete("/", api.deleteOrganization) r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index a3fe444d30135..20295320d0232 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -64,8 +64,8 @@ func TestCustomOrganizationRole(t *testing.T) { // TODO: At present user roles are not returned by the user endpoints. // Changing this might mess up the UI in how it renders the roles on the // users page. When the users endpoint is updated, this should be uncommented. - //roleNamesF := func(role codersdk.SlimRole) string { return role.Name } - //require.Contains(t, db2sdk.List(user.Roles, roleNamesF), role.Name) + // roleNamesF := func(role codersdk.SlimRole) string { return role.Name } + // require.Contains(t, db2sdk.List(user.Roles, roleNamesF), role.Name) // Try to create a template version coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil) From 5eaf8683d9cd5f92c42375715e3eb04a79658415 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 24 May 2024 09:40:47 -1000 Subject: [PATCH 06/14] make gen --- coderd/apidoc/docs.go | 10 ++++++++++ coderd/apidoc/swagger.json | 10 ++++++++++ coderd/roles.go | 1 + docs/api/members.md | 6 ++++++ 4 files changed, 27 insertions(+) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1cb08303ba25b..291ac14d42f02 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2240,6 +2240,16 @@ const docTemplate = `{ ], "summary": "Upsert a custom organization role", "operationId": "upsert-a-custom-organization-role", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 3f2eb4c33df3e..30179bc9256ad 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1953,6 +1953,16 @@ "tags": ["Members"], "summary": "Upsert a custom organization role", "operationId": "upsert-a-custom-organization-role", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", diff --git a/coderd/roles.go b/coderd/roles.go index a85c0a354f602..d4490f84b6cc8 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -39,6 +39,7 @@ func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, _ databa // @ID upsert-a-custom-organization-role // @Security CoderSessionToken // @Produce json +// @Param organization path string true "Organization ID" format(uuid) // @Tags Members // @Success 200 {array} codersdk.Role // @Router /organizations/{organization}/members/roles [patch] diff --git a/docs/api/members.md b/docs/api/members.md index cd3b139a3e9b3..098e34215b686 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -149,6 +149,12 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/membe `PATCH /organizations/{organization}/members/roles` +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------------ | -------- | --------------- | +| `organization` | path | string(uuid) | true | Organization ID | + ### Example responses > 200 Response From 3f56f51aa60d0607de69cfa81ac37421da4e36e7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 10:27:27 -0500 Subject: [PATCH 07/14] chore: remove org map perms in sdk --- coderd/database/db2sdk/db2sdk.go | 23 +++++++++++++++++------ coderd/database/dbauthz/dbauthz.go | 4 ++++ coderd/roles.go | 8 -------- codersdk/roles.go | 6 +++--- enterprise/coderd/roles.go | 27 ++++++++++++--------------- enterprise/coderd/roles_test.go | 21 ++++++++++----------- 6 files changed, 46 insertions(+), 43 deletions(-) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 590183bd43dd1..2fe9ac9af7a3d 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -531,12 +531,16 @@ func Role(role rbac.Role) codersdk.Role { if err != nil { roleName = role.Name } + return codersdk.Role{ - Name: roleName, - OrganizationID: orgIDStr, - DisplayName: role.DisplayName, - SitePermissions: List(role.Site, Permission), - OrganizationPermissions: Map(role.Org, ListLazy(Permission)), + Name: roleName, + OrganizationID: orgIDStr, + DisplayName: role.DisplayName, + SitePermissions: List(role.Site, Permission), + // This is not perfect. If there are organization permissions in another + // organization, they will be omitted. This should not be allowed, so + // should never happen. + OrganizationPermissions: List(role.Org[orgIDStr], Permission), UserPermissions: List(role.User, Permission), } } @@ -550,11 +554,18 @@ func Permission(permission rbac.Permission) codersdk.Permission { } func RoleToRBAC(role codersdk.Role) rbac.Role { + orgPerms := map[string][]rbac.Permission{} + if role.OrganizationID != "" { + orgPerms = map[string][]rbac.Permission{ + role.OrganizationID: List(role.OrganizationPermissions, PermissionToRBAC), + } + } + return rbac.Role{ Name: rbac.RoleName(role.Name, role.OrganizationID), DisplayName: role.DisplayName, Site: List(role.SitePermissions, PermissionToRBAC), - Org: Map(role.OrganizationPermissions, ListLazy(PermissionToRBAC)), + Org: orgPerms, User: List(role.UserPermissions, PermissionToRBAC), } } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1c0b728f4d843..ec9d14bb57de6 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -614,6 +614,10 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r return xerrors.Errorf("role %q has invalid uuid for org: %w", r, err) } + if orgID == nil { + return xerrors.Errorf("should never happen, orgID is nil, but trying to assign an organization role") + } + if roleOrgID != *orgID { return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, orgID.String()) } diff --git a/coderd/roles.go b/coderd/roles.go index d4490f84b6cc8..e8505baa4d255 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -55,14 +55,6 @@ func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) { return } - if err := httpapi.NameValid(req.Name); err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid role name", - Detail: err.Error(), - }) - return - } - updated, ok := handler.PatchOrganizationRole(ctx, api.Database, rw, organization.ID, req) if !ok { return diff --git a/codersdk/roles.go b/codersdk/roles.go index 2fbe304de25c2..8b119e935a6c6 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -39,9 +39,9 @@ type Role struct { OrganizationID string `json:"organization_id" table:"organization_id" format:"uuid"` DisplayName string `json:"display_name" table:"display_name"` SitePermissions []Permission `json:"site_permissions" table:"site_permissions"` - // map[] -> Permissions - OrganizationPermissions map[string][]Permission `json:"organization_permissions" table:"org_permissions"` - UserPermissions []Permission `json:"user_permissions" table:"user_permissions"` + // OrganizationPermissions are specific for the organization in the field 'OrganizationID' above. + OrganizationPermissions []Permission `json:"organization_permissions" table:"org_permissions"` + UserPermissions []Permission `json:"user_permissions" table:"user_permissions"` } // FullName returns the role name scoped to the organization ID. This is useful if diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index 3e76df8001ba7..bc1cc19a674e1 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -21,7 +21,15 @@ type enterpriseCustomRoleHandler struct { func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) { if !h.Enabled { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: "Custom roles is not enabled", + Message: "Custom roles are not enabled", + }) + return codersdk.Role{}, false + } + + if err := httpapi.NameValid(role.Name); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid role name", + Detail: err.Error(), }) return codersdk.Role{}, false } @@ -43,25 +51,14 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, return codersdk.Role{}, false } - if len(role.OrganizationPermissions) > 1 { + if role.OrganizationID != orgID.String() { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid request, Only 1 organization can be assigned permissions", - Detail: "roles can only contain 1 organization", + Message: "Invalid request, organization in role and url must match", + Detail: fmt.Sprintf("role org %q does not match URL %q", role.OrganizationID, orgID.String()), }) return codersdk.Role{}, false } - if len(role.OrganizationPermissions) == 1 { - _, exists := role.OrganizationPermissions[orgID.String()] - if !exists { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Invalid request, expected permissions for only the organization %q", orgID.String()), - Detail: fmt.Sprintf("only org id %s allowed", orgID.String()), - }) - return codersdk.Role{}, false - } - } - // Make sure all permissions inputted are valid according to our policy. rbacRole := db2sdk.RoleToRBAC(role) args, err := rolestore.ConvertRoleToDB(rbacRole) diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 20295320d0232..44825ae3733a5 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -20,17 +20,16 @@ func TestCustomOrganizationRole(t *testing.T) { t.Parallel() templateAdminCustom := func(orgID uuid.UUID) codersdk.Role { return codersdk.Role{ - Name: "test-role", - DisplayName: "Testing Purposes", + Name: "test-role", + DisplayName: "Testing Purposes", + OrganizationID: orgID.String(), // Basically creating a template admin manually SitePermissions: nil, - OrganizationPermissions: map[string][]codersdk.Permission{ - orgID.String(): codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ - codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights}, - codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead}, - codersdk.ResourceWorkspace: {codersdk.ActionRead}, - }), - }, + OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights}, + codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead}, + codersdk.ResourceWorkspace: {codersdk.ActionRead}, + }), UserPermissions: nil, } } @@ -84,7 +83,7 @@ func TestCustomOrganizationRole(t *testing.T) { }), "role missing from org role list") require.Len(t, foundRole.SitePermissions, 0) - require.Len(t, foundRole.OrganizationPermissions[first.OrganizationID.String()], 7) + require.Len(t, foundRole.OrganizationPermissions, 7) require.Len(t, foundRole.UserPermissions, 0) }) @@ -122,7 +121,7 @@ func TestCustomOrganizationRole(t *testing.T) { // Verify functionality is lost _, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID)) - require.ErrorContains(t, err, "roles is not enabled") + require.ErrorContains(t, err, "roles are not enabled") // Assign the custom template admin role tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName()) From 715efa2b7bf6ffe2c6b273af1acb11ec4a30227f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 11:11:50 -0500 Subject: [PATCH 08/14] add test for mismatched orgID --- enterprise/coderd/roles.go | 2 +- enterprise/coderd/roles_test.go | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index bc1cc19a674e1..448ec9f855cc0 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -54,7 +54,7 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, if role.OrganizationID != orgID.String() { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid request, organization in role and url must match", - Detail: fmt.Sprintf("role org %q does not match URL %q", role.OrganizationID, orgID.String()), + Detail: fmt.Sprintf("role organization=%q does not match URL=%q", role.OrganizationID, orgID.String()), }) return codersdk.Role{}, false } diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 44825ae3733a5..f3846c2b50fe8 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -89,7 +89,7 @@ func TestCustomOrganizationRole(t *testing.T) { // Revoked licenses cannot modify/create custom roles, but they can // use the existing roles. - t.Run("Revoked License", func(t *testing.T) { + t.Run("RevokedLicense", func(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} @@ -208,4 +208,26 @@ func TestCustomOrganizationRole(t *testing.T) { }) require.ErrorContains(t, err, "Validation") }) + + t.Run("MismatchedOrganizations", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + + //nolint:gocritic // owner is required for this + _, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(uuid.New())) + require.ErrorContains(t, err, "does not match") + }) } From 816f97ca7f04598441663c232a2eecef8919355a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 11:22:42 -0500 Subject: [PATCH 09/14] add unit tests for site & user permission attempt --- enterprise/coderd/roles_test.go | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index f3846c2b50fe8..199cb46824340 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -230,4 +230,47 @@ func TestCustomOrganizationRole(t *testing.T) { _, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(uuid.New())) require.ErrorContains(t, err, "does not match") }) + + // Attempt to add site & user permissions, which is not allowed + t.Run("ExcessPermissions", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + + siteRole := templateAdminCustom(first.OrganizationID) + siteRole.SitePermissions = []codersdk.Permission{ + { + ResourceType: codersdk.ResourceWorkspace, + Action: codersdk.ActionRead, + }, + } + + //nolint:gocritic // owner is required for this + _, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, siteRole) + require.ErrorContains(t, err, "site wide permissions") + + userRole := templateAdminCustom(first.OrganizationID) + userRole.UserPermissions = []codersdk.Permission{ + { + ResourceType: codersdk.ResourceWorkspace, + Action: codersdk.ActionRead, + }, + } + + //nolint:gocritic // owner is required for this + _, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, userRole) + require.ErrorContains(t, err, "not allowed to assign user permissions") + }) } From 6e88678190b8a203186337d307e0483ad55325bd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 11:25:02 -0500 Subject: [PATCH 10/14] add invalid uuid test --- enterprise/coderd/roles_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 199cb46824340..e1d6855aff002 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -273,4 +273,29 @@ func TestCustomOrganizationRole(t *testing.T) { _, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, userRole) require.ErrorContains(t, err, "not allowed to assign user permissions") }) + + t.Run("InvalidUUID", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + }, + }, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + + newRole := templateAdminCustom(first.OrganizationID) + newRole.OrganizationID = "0000" // This is not a valid uuid + + //nolint:gocritic // owner is required for this + _, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, newRole) + require.ErrorContains(t, err, "Invalid request") + }) } From 282f261d5fcc41f8d7b8fc77424ac0d9a4f84c2e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 11:49:01 -0500 Subject: [PATCH 11/14] make gen --- coderd/apidoc/docs.go | 24 ++---- coderd/apidoc/swagger.json | 24 ++---- docs/api/members.md | 152 +++++++++++++-------------------- docs/api/schemas.md | 86 +++++++------------ site/src/api/typesGenerated.ts | 2 +- 5 files changed, 113 insertions(+), 175 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 291ac14d42f02..c274a608ba240 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8436,13 +8436,10 @@ const docTemplate = `{ "format": "uuid" }, "organization_permissions": { - "description": "map[\u003corg_id\u003e] -\u003e Permissions", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Permission" - } + "description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Permission" } }, "site_permissions": { @@ -11260,13 +11257,10 @@ const docTemplate = `{ "format": "uuid" }, "organization_permissions": { - "description": "map[\u003corg_id\u003e] -\u003e Permissions", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Permission" - } + "description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Permission" } }, "site_permissions": { @@ -14507,7 +14501,7 @@ const docTemplate = `{ "type": "string" }, "host": { - "description": "host or host:port (see Hostname and Port methods)", + "description": "host or host:port", "type": "string" }, "omitHost": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 30179bc9256ad..5c8d1984f5d91 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7491,13 +7491,10 @@ "format": "uuid" }, "organization_permissions": { - "description": "map[\u003corg_id\u003e] -\u003e Permissions", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Permission" - } + "description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Permission" } }, "site_permissions": { @@ -10152,13 +10149,10 @@ "format": "uuid" }, "organization_permissions": { - "description": "map[\u003corg_id\u003e] -\u003e Permissions", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Permission" - } + "description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Permission" } }, "site_permissions": { @@ -13223,7 +13217,7 @@ "type": "string" }, "host": { - "description": "host or host:port (see Hostname and Port methods)", + "description": "host or host:port", "type": "string" }, "omitHost": { diff --git a/docs/api/members.md b/docs/api/members.md index 098e34215b686..6364b08ca528e 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -31,22 +31,13 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "organization_permissions": { - "property1": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ], - "property2": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ] - }, + "organization_permissions": [ + { + "action": "application_connect", + "negate": true, + "resource_type": "*" + } + ], "site_permissions": [ { "action": "application_connect", @@ -75,21 +66,20 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- | -| `[array item]` | array | false | | | -| `» assignable` | boolean | false | | | -| `» built_in` | boolean | false | | Built in roles are immutable | -| `» display_name` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» organization_permissions` | object | false | | map[] -> Permissions | -| `»» [any property]` | array | false | | | -| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | -| `»»» negate` | boolean | false | | Negate makes this a negative permission | -| `»»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | | -| `» site_permissions` | array | false | | | -| `» user_permissions` | array | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» assignable` | boolean | false | | | +| `» built_in` | boolean | false | | Built in roles are immutable | +| `» display_name` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. | +| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | +| `»» negate` | boolean | false | | Negate makes this a negative permission | +| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | | +| `» site_permissions` | array | false | | | +| `» user_permissions` | array | false | | | #### Enumerated Values @@ -165,22 +155,13 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/membe "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "organization_permissions": { - "property1": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ], - "property2": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ] - }, + "organization_permissions": [ + { + "action": "application_connect", + "negate": true, + "resource_type": "*" + } + ], "site_permissions": [ { "action": "application_connect", @@ -209,19 +190,18 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/membe Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- | -| `[array item]` | array | false | | | -| `» display_name` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» organization_permissions` | object | false | | map[] -> Permissions | -| `»» [any property]` | array | false | | | -| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | -| `»»» negate` | boolean | false | | Negate makes this a negative permission | -| `»»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | | -| `» site_permissions` | array | false | | | -| `» user_permissions` | array | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» display_name` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. | +| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | +| `»» negate` | boolean | false | | Negate makes this a negative permission | +| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | | +| `» site_permissions` | array | false | | | +| `» user_permissions` | array | false | | | #### Enumerated Values @@ -350,22 +330,13 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \ "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "organization_permissions": { - "property1": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ], - "property2": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ] - }, + "organization_permissions": [ + { + "action": "application_connect", + "negate": true, + "resource_type": "*" + } + ], "site_permissions": [ { "action": "application_connect", @@ -394,21 +365,20 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- | -| `[array item]` | array | false | | | -| `» assignable` | boolean | false | | | -| `» built_in` | boolean | false | | Built in roles are immutable | -| `» display_name` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» organization_permissions` | object | false | | map[] -> Permissions | -| `»» [any property]` | array | false | | | -| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | -| `»»» negate` | boolean | false | | Negate makes this a negative permission | -| `»»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | | -| `» site_permissions` | array | false | | | -| `» user_permissions` | array | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» assignable` | boolean | false | | | +| `» built_in` | boolean | false | | Built in roles are immutable | +| `» display_name` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. | +| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | +| `»» negate` | boolean | false | | Negate makes this a negative permission | +| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | | +| `» site_permissions` | array | false | | | +| `» user_permissions` | array | false | | | #### Enumerated Values diff --git a/docs/api/schemas.md b/docs/api/schemas.md index ca7493ae53ec0..679081782fca3 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -806,22 +806,13 @@ "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "organization_permissions": { - "property1": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ], - "property2": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ] - }, + "organization_permissions": [ + { + "action": "application_connect", + "negate": true, + "resource_type": "*" + } + ], "site_permissions": [ { "action": "application_connect", @@ -841,17 +832,16 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- | -| `assignable` | boolean | false | | | -| `built_in` | boolean | false | | Built in roles are immutable | -| `display_name` | string | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `organization_permissions` | object | false | | map[] -> Permissions | -| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | | -| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | -| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------------------- | --------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `assignable` | boolean | false | | | +| `built_in` | boolean | false | | Built in roles are immutable | +| `display_name` | string | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. | +| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | +| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | ## codersdk.AuditAction @@ -4330,22 +4320,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "display_name": "string", "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "organization_permissions": { - "property1": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ], - "property2": [ - { - "action": "application_connect", - "negate": true, - "resource_type": "*" - } - ] - }, + "organization_permissions": [ + { + "action": "application_connect", + "negate": true, + "resource_type": "*" + } + ], "site_permissions": [ { "action": "application_connect", @@ -4365,15 +4346,14 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- | -| `display_name` | string | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `organization_permissions` | object | false | | map[] -> Permissions | -| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | | -| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | -| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------------------- | --------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `display_name` | string | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. | +| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | +| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | ## codersdk.SSHConfig @@ -8927,7 +8907,7 @@ _None_ | ------------- | ---------------------------- | -------- | ------------ | -------------------------------------------------- | | `forceQuery` | boolean | false | | append a query ('?') even if RawQuery is empty | | `fragment` | string | false | | fragment for references, without '#' | -| `host` | string | false | | host or host:port (see Hostname and Port methods) | +| `host` | string | false | | host or host:port | | `omitHost` | boolean | false | | do not emit empty host (authority) | | `opaque` | string | false | | encoded opaque data | | `path` | string | false | | path (relative paths may omit leading slash) | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5d4d148758f36..171f6744680cb 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -980,7 +980,7 @@ export interface Role { readonly organization_id: string; readonly display_name: string; readonly site_permissions: readonly Permission[]; - readonly organization_permissions: Record; + readonly organization_permissions: readonly Permission[]; readonly user_permissions: readonly Permission[]; } From d633afcb4e1f881131808b1361b409697f2f59ef Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 12:03:31 -0500 Subject: [PATCH 12/14] fix ts --- site/src/testHelpers/entities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 1fbb18aa86a07..630b7ec0f0378 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -233,7 +233,7 @@ export const MockOwnerRole: TypesGen.Role = { name: "owner", display_name: "Owner", site_permissions: [], - organization_permissions: {}, + organization_permissions: [], user_permissions: [], organization_id: "", }; @@ -242,7 +242,7 @@ export const MockUserAdminRole: TypesGen.Role = { name: "user_admin", display_name: "User Admin", site_permissions: [], - organization_permissions: {}, + organization_permissions: [], user_permissions: [], organization_id: "", }; From 5ac97f87a59f19ecf0798df86d53db94bcd8136b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 12:06:31 -0500 Subject: [PATCH 13/14] fixup! fix ts --- site/src/testHelpers/entities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 630b7ec0f0378..5ff5fa6cd84c7 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -251,7 +251,7 @@ export const MockTemplateAdminRole: TypesGen.Role = { name: "template_admin", display_name: "Template Admin", site_permissions: [], - organization_permissions: {}, + organization_permissions: [], user_permissions: [], organization_id: "", }; @@ -265,7 +265,7 @@ export const MockAuditorRole: TypesGen.Role = { name: "auditor", display_name: "Auditor", site_permissions: [], - organization_permissions: {}, + organization_permissions: [], user_permissions: [], organization_id: "", }; From c6335cf2acda2d4b99e565c7b721399471f1ce9e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 28 May 2024 14:32:12 -0500 Subject: [PATCH 14/14] make gen --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- docs/api/schemas.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c274a608ba240..f373e0079a780 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14501,7 +14501,7 @@ const docTemplate = `{ "type": "string" }, "host": { - "description": "host or host:port", + "description": "host or host:port (see Hostname and Port methods)", "type": "string" }, "omitHost": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5c8d1984f5d91..84bb41c44fcdd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13217,7 +13217,7 @@ "type": "string" }, "host": { - "description": "host or host:port", + "description": "host or host:port (see Hostname and Port methods)", "type": "string" }, "omitHost": { diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 679081782fca3..978da35a58d02 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -8907,7 +8907,7 @@ _None_ | ------------- | ---------------------------- | -------- | ------------ | -------------------------------------------------- | | `forceQuery` | boolean | false | | append a query ('?') even if RawQuery is empty | | `fragment` | string | false | | fragment for references, without '#' | -| `host` | string | false | | host or host:port | +| `host` | string | false | | host or host:port (see Hostname and Port methods) | | `omitHost` | boolean | false | | do not emit empty host (authority) | | `opaque` | string | false | | encoded opaque data | | `path` | string | false | | path (relative paths may omit leading slash) |