mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: remove UpsertCustomRole in favor of Insert + Update (#14217)
* chore: remove UpsertCustomRole in favor of Insert + Update --------- Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
@ -153,6 +153,7 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
||||
return err
|
||||
}
|
||||
|
||||
createNewRole := true
|
||||
var customRole codersdk.Role
|
||||
if jsonInput {
|
||||
// JSON Upload mode
|
||||
@ -174,17 +175,30 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
||||
}
|
||||
return xerrors.Errorf("json input does not appear to be a valid role")
|
||||
}
|
||||
|
||||
existingRoles, err := client.ListOrganizationRoles(ctx, org.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("listing existing roles: %w", err)
|
||||
}
|
||||
for _, existingRole := range existingRoles {
|
||||
if strings.EqualFold(customRole.Name, existingRole.Name) {
|
||||
// Editing an existing role
|
||||
createNewRole = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(inv.Args) == 0 {
|
||||
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles edit <role_name>\"")
|
||||
}
|
||||
|
||||
interactiveRole, err := interactiveOrgRoleEdit(inv, org.ID, client)
|
||||
interactiveRole, newRole, err := interactiveOrgRoleEdit(inv, org.ID, client)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("editing role: %w", err)
|
||||
}
|
||||
|
||||
customRole = *interactiveRole
|
||||
createNewRole = newRole
|
||||
|
||||
preview := fmt.Sprintf("permissions: %d site, %d org, %d user",
|
||||
len(customRole.SitePermissions), len(customRole.OrganizationPermissions), len(customRole.UserPermissions))
|
||||
@ -203,7 +217,12 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
||||
// Do not actually post
|
||||
updated = customRole
|
||||
} else {
|
||||
updated, err = client.PatchOrganizationRole(ctx, customRole)
|
||||
switch createNewRole {
|
||||
case true:
|
||||
updated, err = client.CreateOrganizationRole(ctx, customRole)
|
||||
default:
|
||||
updated, err = client.UpdateOrganizationRole(ctx, customRole)
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("patch role: %w", err)
|
||||
}
|
||||
@ -223,11 +242,12 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
||||
return cmd
|
||||
}
|
||||
|
||||
func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, client *codersdk.Client) (*codersdk.Role, error) {
|
||||
func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, client *codersdk.Client) (*codersdk.Role, bool, error) {
|
||||
newRole := false
|
||||
ctx := inv.Context()
|
||||
roles, err := client.ListOrganizationRoles(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("listing roles: %w", err)
|
||||
return nil, newRole, xerrors.Errorf("listing roles: %w", err)
|
||||
}
|
||||
|
||||
// Make sure the role actually exists first
|
||||
@ -246,22 +266,23 @@ func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, client *co
|
||||
IsConfirm: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("abort: %w", err)
|
||||
return nil, newRole, xerrors.Errorf("abort: %w", err)
|
||||
}
|
||||
|
||||
originalRole.Role = codersdk.Role{
|
||||
Name: inv.Args[0],
|
||||
OrganizationID: orgID.String(),
|
||||
}
|
||||
newRole = true
|
||||
}
|
||||
|
||||
// Some checks since interactive mode is limited in what it currently sees
|
||||
if len(originalRole.SitePermissions) > 0 {
|
||||
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains site wide permissions")
|
||||
return nil, newRole, xerrors.Errorf("unable to edit role in interactive mode, it contains site wide permissions")
|
||||
}
|
||||
|
||||
if len(originalRole.UserPermissions) > 0 {
|
||||
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
|
||||
return nil, newRole, xerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
|
||||
}
|
||||
|
||||
role := &originalRole.Role
|
||||
@ -283,13 +304,13 @@ customRoleLoop:
|
||||
Options: append(permissionPreviews(role, allowedResources), done, abort),
|
||||
})
|
||||
if err != nil {
|
||||
return role, xerrors.Errorf("selecting resource: %w", err)
|
||||
return role, newRole, xerrors.Errorf("selecting resource: %w", err)
|
||||
}
|
||||
switch selected {
|
||||
case done:
|
||||
break customRoleLoop
|
||||
case abort:
|
||||
return role, xerrors.Errorf("edit role %q aborted", role.Name)
|
||||
return role, newRole, xerrors.Errorf("edit role %q aborted", role.Name)
|
||||
default:
|
||||
strs := strings.Split(selected, "::")
|
||||
resource := strings.TrimSpace(strs[0])
|
||||
@ -300,7 +321,7 @@ customRoleLoop:
|
||||
Defaults: defaultActions(role, resource),
|
||||
})
|
||||
if err != nil {
|
||||
return role, xerrors.Errorf("selecting actions for resource %q: %w", resource, err)
|
||||
return role, newRole, xerrors.Errorf("selecting actions for resource %q: %w", resource, err)
|
||||
}
|
||||
applyOrgResourceActions(role, resource, actions)
|
||||
// back to resources!
|
||||
@ -309,7 +330,7 @@ customRoleLoop:
|
||||
// This println is required because the prompt ends us on the same line as some text.
|
||||
_, _ = fmt.Println()
|
||||
|
||||
return role, nil
|
||||
return role, newRole, nil
|
||||
}
|
||||
|
||||
func applyOrgResourceActions(role *codersdk.Role, resource string, actions []string) {
|
||||
|
112
coderd/apidoc/docs.go
generated
112
coderd/apidoc/docs.go
generated
@ -2500,7 +2500,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
@ -2532,7 +2532,55 @@ const docTemplate = `{
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.PatchRoleRequest"
|
||||
"$ref": "#/definitions/codersdk.CustomRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Members"
|
||||
],
|
||||
"summary": "Insert a custom organization role",
|
||||
"operationId": "insert-a-custom-organization-role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Insert role request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CustomRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -9455,6 +9503,36 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CustomRoleRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific to the organization the role belongs to.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DAUEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -11071,36 +11149,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PatchRoleRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific to the organization the role belongs to.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PatchTemplateVersionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
106
coderd/apidoc/swagger.json
generated
106
coderd/apidoc/swagger.json
generated
@ -2184,7 +2184,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
@ -2210,7 +2210,49 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.PatchRoleRequest"
|
||||
"$ref": "#/definitions/codersdk.CustomRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Members"],
|
||||
"summary": "Insert a custom organization role",
|
||||
"operationId": "insert-a-custom-organization-role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Insert role request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CustomRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -8417,6 +8459,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CustomRoleRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific to the organization the role belongs to.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DAUEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -9975,36 +10047,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PatchRoleRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific to the organization the role belongs to.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PatchTemplateVersionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -19,8 +19,8 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// TestUpsertCustomRoles verifies creating custom roles cannot escalate permissions.
|
||||
func TestUpsertCustomRoles(t *testing.T) {
|
||||
// TestInsertCustomRoles verifies creating custom roles cannot escalate permissions.
|
||||
func TestInsertCustomRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userID := uuid.New()
|
||||
@ -98,7 +98,7 @@ func TestUpsertCustomRoles(t *testing.T) {
|
||||
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||
}),
|
||||
errorContains: "cannot assign both org and site permissions",
|
||||
errorContains: "organization roles specify site or user permissions",
|
||||
},
|
||||
{
|
||||
name: "invalid-action",
|
||||
@ -231,7 +231,7 @@ func TestUpsertCustomRoles(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
ctx = dbauthz.As(ctx, subject)
|
||||
|
||||
_, err := az.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
_, err := az.InsertCustomRole(ctx, database.InsertCustomRoleParams{
|
||||
Name: "test-role",
|
||||
DisplayName: "",
|
||||
OrganizationID: tc.organizationID,
|
||||
|
@ -815,6 +815,86 @@ func (q *querier) customRoleEscalationCheck(ctx context.Context, actor rbac.Subj
|
||||
return nil
|
||||
}
|
||||
|
||||
// customRoleCheck will validate a custom role for inserting or updating.
|
||||
// If the role is not valid, an error will be returned.
|
||||
// - Check custom roles are valid for their resource types + actions
|
||||
// - Check the actor can create the custom role
|
||||
// - Check the custom role does not grant perms the actor does not have
|
||||
// - Prevent negative perms
|
||||
// - Prevent roles with site and org permissions.
|
||||
func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole) error {
|
||||
act, ok := ActorFromContext(ctx)
|
||||
if !ok {
|
||||
return NoActorError
|
||||
}
|
||||
|
||||
// Org permissions require an org role
|
||||
if role.OrganizationID.UUID == uuid.Nil && len(role.OrgPermissions) > 0 {
|
||||
return xerrors.Errorf("organization permissions require specifying an organization id")
|
||||
}
|
||||
|
||||
// Org roles can only specify org permissions
|
||||
if role.OrganizationID.UUID != uuid.Nil && (len(role.SitePermissions) > 0 || len(role.UserPermissions) > 0) {
|
||||
return xerrors.Errorf("organization roles specify site or user permissions")
|
||||
}
|
||||
|
||||
// The rbac.Role has a 'Valid()' function on it that will do a lot
|
||||
// of checks.
|
||||
rbacRole, err := rolestore.ConvertDBRole(database.CustomRole{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: role.SitePermissions,
|
||||
OrgPermissions: role.OrgPermissions,
|
||||
UserPermissions: role.UserPermissions,
|
||||
OrganizationID: role.OrganizationID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("invalid args: %w", err)
|
||||
}
|
||||
|
||||
err = rbacRole.Valid()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("invalid role: %w", err)
|
||||
}
|
||||
|
||||
if len(rbacRole.Org) > 0 && len(rbacRole.Site) > 0 {
|
||||
// This is a choice to keep roles simple. If we allow mixing site and org scoped perms, then knowing who can
|
||||
// do what gets more complicated.
|
||||
return xerrors.Errorf("invalid custom role, cannot assign both org and site permissions at the same time")
|
||||
}
|
||||
|
||||
if len(rbacRole.Org) > 1 {
|
||||
// Again to avoid more complexity in our roles
|
||||
return xerrors.Errorf("invalid custom role, cannot assign permissions to more than 1 org at a time")
|
||||
}
|
||||
|
||||
// Prevent escalation
|
||||
for _, sitePerm := range rbacRole.Site {
|
||||
err := q.customRoleEscalationCheck(ctx, act, sitePerm, rbac.Object{Type: sitePerm.ResourceType})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("site permission: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for orgID, perms := range rbacRole.Org {
|
||||
for _, orgPerm := range perms {
|
||||
err := q.customRoleEscalationCheck(ctx, act, orgPerm, rbac.Object{OrgID: orgID, Type: orgPerm.ResourceType})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("org=%q: %w", orgID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, userPerm := range rbacRole.User {
|
||||
err := q.customRoleEscalationCheck(ctx, act, userPerm, rbac.Object{Type: userPerm.ResourceType, Owner: act.ID})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("user permission: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *querier) AcquireLock(ctx context.Context, id int64) error {
|
||||
return q.db.AcquireLock(ctx, id)
|
||||
}
|
||||
@ -2551,6 +2631,34 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
|
||||
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
|
||||
// Org and site role upsert share the same query. So switch the assertion based on the org uuid.
|
||||
if arg.OrganizationID.UUID != uuid.Nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
} else {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignRole); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.customRoleCheck(ctx, database.CustomRole{
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
SitePermissions: arg.SitePermissions,
|
||||
OrgPermissions: arg.OrgPermissions,
|
||||
UserPermissions: arg.UserPermissions,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
OrganizationID: arg.OrganizationID,
|
||||
ID: uuid.New(),
|
||||
}); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
return q.db.InsertCustomRole(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertDBCryptKey(ctx context.Context, arg database.InsertDBCryptKeyParams) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
|
||||
return err
|
||||
@ -3002,6 +3110,33 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
|
||||
if arg.OrganizationID.UUID != uuid.Nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
} else {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignRole); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.customRoleCheck(ctx, database.CustomRole{
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
SitePermissions: arg.SitePermissions,
|
||||
OrgPermissions: arg.OrgPermissions,
|
||||
UserPermissions: arg.UserPermissions,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
OrganizationID: arg.OrganizationID,
|
||||
ID: uuid.New(),
|
||||
}); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
return q.db.UpdateCustomRole(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
|
||||
@ -3664,91 +3799,6 @@ func (q *querier) UpsertApplicationName(ctx context.Context, value string) error
|
||||
return q.db.UpsertApplicationName(ctx, value)
|
||||
}
|
||||
|
||||
// UpsertCustomRole does a series of authz checks to protect custom roles.
|
||||
// - Check custom roles are valid for their resource types + actions
|
||||
// - Check the actor can create the custom role
|
||||
// - Check the custom role does not grant perms the actor does not have
|
||||
// - Prevent negative perms
|
||||
// - Prevent roles with site and org permissions.
|
||||
func (q *querier) UpsertCustomRole(ctx context.Context, arg database.UpsertCustomRoleParams) (database.CustomRole, error) {
|
||||
act, ok := ActorFromContext(ctx)
|
||||
if !ok {
|
||||
return database.CustomRole{}, NoActorError
|
||||
}
|
||||
|
||||
// Org and site role upsert share the same query. So switch the assertion based on the org uuid.
|
||||
if arg.OrganizationID.UUID != uuid.Nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
} else {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignRole); err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if arg.OrganizationID.UUID == uuid.Nil && len(arg.OrgPermissions) > 0 {
|
||||
return database.CustomRole{}, xerrors.Errorf("organization permissions require specifying an organization id")
|
||||
}
|
||||
|
||||
// There is quite a bit of validation we should do here.
|
||||
// The rbac.Role has a 'Valid()' function on it that will do a lot
|
||||
// of checks.
|
||||
rbacRole, err := rolestore.ConvertDBRole(database.CustomRole{
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
SitePermissions: arg.SitePermissions,
|
||||
OrgPermissions: arg.OrgPermissions,
|
||||
UserPermissions: arg.UserPermissions,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
})
|
||||
if err != nil {
|
||||
return database.CustomRole{}, xerrors.Errorf("invalid args: %w", err)
|
||||
}
|
||||
|
||||
err = rbacRole.Valid()
|
||||
if err != nil {
|
||||
return database.CustomRole{}, xerrors.Errorf("invalid role: %w", err)
|
||||
}
|
||||
|
||||
if len(rbacRole.Org) > 0 && len(rbacRole.Site) > 0 {
|
||||
// This is a choice to keep roles simple. If we allow mixing site and org scoped perms, then knowing who can
|
||||
// do what gets more complicated.
|
||||
return database.CustomRole{}, xerrors.Errorf("invalid custom role, cannot assign both org and site permissions at the same time")
|
||||
}
|
||||
|
||||
if len(rbacRole.Org) > 1 {
|
||||
// Again to avoid more complexity in our roles
|
||||
return database.CustomRole{}, xerrors.Errorf("invalid custom role, cannot assign permissions to more than 1 org at a time")
|
||||
}
|
||||
|
||||
// Prevent escalation
|
||||
for _, sitePerm := range rbacRole.Site {
|
||||
err := q.customRoleEscalationCheck(ctx, act, sitePerm, rbac.Object{Type: sitePerm.ResourceType})
|
||||
if err != nil {
|
||||
return database.CustomRole{}, xerrors.Errorf("site permission: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for orgID, perms := range rbacRole.Org {
|
||||
for _, orgPerm := range perms {
|
||||
err := q.customRoleEscalationCheck(ctx, act, orgPerm, rbac.Object{OrgID: orgID, Type: orgPerm.ResourceType})
|
||||
if err != nil {
|
||||
return database.CustomRole{}, xerrors.Errorf("org=%q: %w", orgID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, userPerm := range rbacRole.User {
|
||||
err := q.customRoleEscalationCheck(ctx, act, userPerm, rbac.Object{Type: userPerm.ResourceType, Owner: act.ID})
|
||||
if err != nil {
|
||||
return database.CustomRole{}, xerrors.Errorf("user permission: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return q.db.UpsertCustomRole(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDefaultProxyParams) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return err
|
||||
|
@ -1282,9 +1282,77 @@ func (s *MethodTestSuite) TestUser() {
|
||||
}).Asserts(
|
||||
rbac.ResourceAssignRole, policy.ActionDelete)
|
||||
}))
|
||||
s.Run("Blank/UpsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
s.Run("Blank/UpdateCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{})
|
||||
// Blank is no perms in the role
|
||||
check.Args(database.UpsertCustomRoleParams{
|
||||
check.Args(database.UpdateCustomRoleParams{
|
||||
Name: customRole.Name,
|
||||
DisplayName: "Test Name",
|
||||
SitePermissions: nil,
|
||||
OrgPermissions: nil,
|
||||
UserPermissions: nil,
|
||||
}).Asserts(rbac.ResourceAssignRole, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("SitePermissions/UpdateCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: uuid.Nil,
|
||||
Valid: false,
|
||||
},
|
||||
})
|
||||
check.Args(database.UpdateCustomRoleParams{
|
||||
Name: customRole.Name,
|
||||
OrganizationID: customRole.OrganizationID,
|
||||
DisplayName: "Test Name",
|
||||
SitePermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionDelete, codersdk.ActionViewInsights},
|
||||
}), convertSDKPerm),
|
||||
OrgPermissions: nil,
|
||||
UserPermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||
}), convertSDKPerm),
|
||||
}).Asserts(
|
||||
// First check
|
||||
rbac.ResourceAssignRole, policy.ActionUpdate,
|
||||
// Escalation checks
|
||||
rbac.ResourceTemplate, policy.ActionCreate,
|
||||
rbac.ResourceTemplate, policy.ActionRead,
|
||||
rbac.ResourceTemplate, policy.ActionUpdate,
|
||||
rbac.ResourceTemplate, policy.ActionDelete,
|
||||
rbac.ResourceTemplate, policy.ActionViewInsights,
|
||||
|
||||
rbac.ResourceWorkspace.WithOwner(testActorID.String()), policy.ActionRead,
|
||||
)
|
||||
}))
|
||||
s.Run("OrgPermissions/UpdateCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
orgID := uuid.New()
|
||||
customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: orgID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
|
||||
check.Args(database.UpdateCustomRoleParams{
|
||||
Name: customRole.Name,
|
||||
DisplayName: "Test Name",
|
||||
OrganizationID: customRole.OrganizationID,
|
||||
SitePermissions: nil,
|
||||
OrgPermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead},
|
||||
}), convertSDKPerm),
|
||||
UserPermissions: nil,
|
||||
}).Asserts(
|
||||
// First check
|
||||
rbac.ResourceAssignOrgRole.InOrg(orgID), policy.ActionUpdate,
|
||||
// Escalation checks
|
||||
rbac.ResourceTemplate.InOrg(orgID), policy.ActionCreate,
|
||||
rbac.ResourceTemplate.InOrg(orgID), policy.ActionRead,
|
||||
)
|
||||
}))
|
||||
s.Run("Blank/InsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
// Blank is no perms in the role
|
||||
check.Args(database.InsertCustomRoleParams{
|
||||
Name: "test",
|
||||
DisplayName: "Test Name",
|
||||
SitePermissions: nil,
|
||||
@ -1292,8 +1360,8 @@ func (s *MethodTestSuite) TestUser() {
|
||||
UserPermissions: nil,
|
||||
}).Asserts(rbac.ResourceAssignRole, policy.ActionCreate)
|
||||
}))
|
||||
s.Run("SitePermissions/UpsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.UpsertCustomRoleParams{
|
||||
s.Run("SitePermissions/InsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.InsertCustomRoleParams{
|
||||
Name: "test",
|
||||
DisplayName: "Test Name",
|
||||
SitePermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
@ -1316,9 +1384,9 @@ func (s *MethodTestSuite) TestUser() {
|
||||
rbac.ResourceWorkspace.WithOwner(testActorID.String()), policy.ActionRead,
|
||||
)
|
||||
}))
|
||||
s.Run("OrgPermissions/UpsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
s.Run("OrgPermissions/InsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
orgID := uuid.New()
|
||||
check.Args(database.UpsertCustomRoleParams{
|
||||
check.Args(database.InsertCustomRoleParams{
|
||||
Name: "test",
|
||||
DisplayName: "Test Name",
|
||||
OrganizationID: uuid.NullUUID{
|
||||
@ -1329,17 +1397,13 @@ func (s *MethodTestSuite) TestUser() {
|
||||
OrgPermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead},
|
||||
}), convertSDKPerm),
|
||||
UserPermissions: db2sdk.List(codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||
}), convertSDKPerm),
|
||||
UserPermissions: nil,
|
||||
}).Asserts(
|
||||
// First check
|
||||
rbac.ResourceAssignOrgRole.InOrg(orgID), policy.ActionCreate,
|
||||
// Escalation checks
|
||||
rbac.ResourceTemplate.InOrg(orgID), policy.ActionCreate,
|
||||
rbac.ResourceTemplate.InOrg(orgID), policy.ActionRead,
|
||||
|
||||
rbac.ResourceWorkspace.WithOwner(testActorID.String()), policy.ActionRead,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
@ -880,7 +880,7 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth
|
||||
}
|
||||
|
||||
func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) database.CustomRole {
|
||||
role, err := db.UpsertCustomRole(genCtx, database.UpsertCustomRoleParams{
|
||||
role, err := db.InsertCustomRole(genCtx, database.InsertCustomRoleParams{
|
||||
Name: takeFirst(seed.Name, strings.ToLower(testutil.GetRandomName(t))),
|
||||
DisplayName: testutil.GetRandomName(t),
|
||||
OrganizationID: seed.OrganizationID,
|
||||
|
@ -6161,6 +6161,37 @@ func (q *FakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAudit
|
||||
return alog, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertCustomRole(_ context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
for i := range q.customRoles {
|
||||
if strings.EqualFold(q.customRoles[i].Name, arg.Name) &&
|
||||
q.customRoles[i].OrganizationID.UUID == arg.OrganizationID.UUID {
|
||||
return database.CustomRole{}, errUniqueConstraint
|
||||
}
|
||||
}
|
||||
|
||||
role := database.CustomRole{
|
||||
ID: uuid.New(),
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
SitePermissions: arg.SitePermissions,
|
||||
OrgPermissions: arg.OrgPermissions,
|
||||
UserPermissions: arg.UserPermissions,
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}
|
||||
q.customRoles = append(q.customRoles, role)
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertDBCryptKey(_ context.Context, arg database.InsertDBCryptKeyParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
@ -7531,6 +7562,29 @@ func (q *FakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateCustomRole(_ context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
for i := range q.customRoles {
|
||||
if strings.EqualFold(q.customRoles[i].Name, arg.Name) &&
|
||||
q.customRoles[i].OrganizationID.UUID == arg.OrganizationID.UUID {
|
||||
q.customRoles[i].DisplayName = arg.DisplayName
|
||||
q.customRoles[i].OrganizationID = arg.OrganizationID
|
||||
q.customRoles[i].SitePermissions = arg.SitePermissions
|
||||
q.customRoles[i].OrgPermissions = arg.OrgPermissions
|
||||
q.customRoles[i].UserPermissions = arg.UserPermissions
|
||||
q.customRoles[i].UpdatedAt = dbtime.Now()
|
||||
return q.customRoles[i], nil
|
||||
}
|
||||
}
|
||||
return database.CustomRole{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateExternalAuthLink(_ context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return database.ExternalAuthLink{}, err
|
||||
@ -8875,42 +8929,6 @@ func (q *FakeQuerier) UpsertApplicationName(_ context.Context, data string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCustomRoleParams) (database.CustomRole, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.CustomRole{}, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
for i := range q.customRoles {
|
||||
if strings.EqualFold(q.customRoles[i].Name, arg.Name) {
|
||||
q.customRoles[i].DisplayName = arg.DisplayName
|
||||
q.customRoles[i].OrganizationID = arg.OrganizationID
|
||||
q.customRoles[i].SitePermissions = arg.SitePermissions
|
||||
q.customRoles[i].OrgPermissions = arg.OrgPermissions
|
||||
q.customRoles[i].UserPermissions = arg.UserPermissions
|
||||
q.customRoles[i].UpdatedAt = dbtime.Now()
|
||||
return q.customRoles[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
role := database.CustomRole{
|
||||
ID: uuid.New(),
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
SitePermissions: arg.SitePermissions,
|
||||
OrgPermissions: arg.OrgPermissions,
|
||||
UserPermissions: arg.UserPermissions,
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}
|
||||
q.customRoles = append(q.customRoles, role)
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpsertDefaultProxy(_ context.Context, arg database.UpsertDefaultProxyParams) error {
|
||||
q.defaultProxyDisplayName = arg.DisplayName
|
||||
q.defaultProxyIconURL = arg.IconUrl
|
||||
|
@ -1586,6 +1586,13 @@ func (m metricsStore) InsertAuditLog(ctx context.Context, arg database.InsertAud
|
||||
return log, err
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertCustomRole(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertCustomRole").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertDBCryptKey(ctx context.Context, arg database.InsertDBCryptKeyParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.InsertDBCryptKey(ctx, arg)
|
||||
@ -1957,6 +1964,13 @@ func (m metricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateA
|
||||
return err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateCustomRole(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateCustomRole").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
start := time.Now()
|
||||
link, err := m.s.UpdateExternalAuthLink(ctx, arg)
|
||||
@ -2370,13 +2384,6 @@ func (m metricsStore) UpsertApplicationName(ctx context.Context, value string) e
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertCustomRole(ctx context.Context, arg database.UpsertCustomRoleParams) (database.CustomRole, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertCustomRole(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertCustomRole").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDefaultProxyParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpsertDefaultProxy(ctx, arg)
|
||||
|
@ -3338,6 +3338,21 @@ func (mr *MockStoreMockRecorder) InsertAuditLog(arg0, arg1 any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAuditLog", reflect.TypeOf((*MockStore)(nil).InsertAuditLog), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertCustomRole mocks base method.
|
||||
func (m *MockStore) InsertCustomRole(arg0 context.Context, arg1 database.InsertCustomRoleParams) (database.CustomRole, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertCustomRole", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.CustomRole)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertCustomRole indicates an expected call of InsertCustomRole.
|
||||
func (mr *MockStoreMockRecorder) InsertCustomRole(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertCustomRole", reflect.TypeOf((*MockStore)(nil).InsertCustomRole), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertDBCryptKey mocks base method.
|
||||
func (m *MockStore) InsertDBCryptKey(arg0 context.Context, arg1 database.InsertDBCryptKeyParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
@ -4130,6 +4145,21 @@ func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(arg0, arg1 any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateCustomRole mocks base method.
|
||||
func (m *MockStore) UpdateCustomRole(arg0 context.Context, arg1 database.UpdateCustomRoleParams) (database.CustomRole, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateCustomRole", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.CustomRole)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateCustomRole indicates an expected call of UpdateCustomRole.
|
||||
func (mr *MockStoreMockRecorder) UpdateCustomRole(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCustomRole", reflect.TypeOf((*MockStore)(nil).UpdateCustomRole), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateExternalAuthLink mocks base method.
|
||||
func (m *MockStore) UpdateExternalAuthLink(arg0 context.Context, arg1 database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -4980,21 +5010,6 @@ func (mr *MockStoreMockRecorder) UpsertApplicationName(arg0, arg1 any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertApplicationName", reflect.TypeOf((*MockStore)(nil).UpsertApplicationName), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpsertCustomRole mocks base method.
|
||||
func (m *MockStore) UpsertCustomRole(arg0 context.Context, arg1 database.UpsertCustomRoleParams) (database.CustomRole, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertCustomRole", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.CustomRole)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpsertCustomRole indicates an expected call of UpsertCustomRole.
|
||||
func (mr *MockStoreMockRecorder) UpsertCustomRole(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertCustomRole", reflect.TypeOf((*MockStore)(nil).UpsertCustomRole), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpsertDefaultProxy mocks base method.
|
||||
func (m *MockStore) UpsertDefaultProxy(arg0 context.Context, arg1 database.UpsertDefaultProxyParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
2
coderd/database/dump.sql
generated
2
coderd/database/dump.sql
generated
@ -1583,7 +1583,7 @@ ALTER TABLE ONLY audit_logs
|
||||
ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY custom_roles
|
||||
ADD CONSTRAINT custom_roles_pkey PRIMARY KEY (name);
|
||||
ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id);
|
||||
|
||||
ALTER TABLE ONLY dbcrypt_keys
|
||||
ADD CONSTRAINT dbcrypt_keys_active_key_digest_key UNIQUE (active_key_digest);
|
||||
|
@ -0,0 +1,5 @@
|
||||
ALTER TABLE custom_roles
|
||||
DROP CONSTRAINT custom_roles_unique_key;
|
||||
|
||||
ALTER TABLE custom_roles
|
||||
ADD CONSTRAINT custom_roles_pkey PRIMARY KEY (name);
|
@ -0,0 +1,6 @@
|
||||
ALTER TABLE custom_roles
|
||||
DROP CONSTRAINT custom_roles_pkey;
|
||||
|
||||
-- Roles are unique to the organization.
|
||||
ALTER TABLE custom_roles
|
||||
ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id);
|
@ -335,6 +335,7 @@ type sqlcQuerier interface {
|
||||
// every member of the org.
|
||||
InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error)
|
||||
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
|
||||
InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error)
|
||||
InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error
|
||||
InsertDERPMeshKey(ctx context.Context, value string) error
|
||||
InsertDeploymentID(ctx context.Context, value string) error
|
||||
@ -402,6 +403,7 @@ type sqlcQuerier interface {
|
||||
UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error
|
||||
UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
|
||||
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
|
||||
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
|
||||
@ -462,7 +464,6 @@ type sqlcQuerier interface {
|
||||
UpsertAnnouncementBanners(ctx context.Context, value string) error
|
||||
UpsertAppSecurityKey(ctx context.Context, value string) error
|
||||
UpsertApplicationName(ctx context.Context, value string) error
|
||||
UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleParams) (CustomRole, error)
|
||||
// The default proxy is implied and not actually stored in the database.
|
||||
// So we need to store it's configuration here for display purposes.
|
||||
// The functional values are immutable and controlled implicitly.
|
||||
|
@ -579,7 +579,7 @@ func TestReadCustomRoles(t *testing.T) {
|
||||
orgID = uuid.NullUUID{}
|
||||
}
|
||||
|
||||
role, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
role, err := db.InsertCustomRole(ctx, database.InsertCustomRoleParams{
|
||||
Name: fmt.Sprintf("role-%d", i),
|
||||
OrganizationID: orgID,
|
||||
})
|
||||
|
@ -6547,7 +6547,7 @@ func (q *sqlQuerier) DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleP
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertCustomRole = `-- name: UpsertCustomRole :one
|
||||
const insertCustomRole = `-- name: InsertCustomRole :one
|
||||
INSERT INTO
|
||||
custom_roles (
|
||||
name,
|
||||
@ -6570,17 +6570,10 @@ VALUES (
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE SET
|
||||
display_name = $2,
|
||||
site_permissions = $4,
|
||||
org_permissions = $5,
|
||||
user_permissions = $6,
|
||||
updated_at = now()
|
||||
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id
|
||||
`
|
||||
|
||||
type UpsertCustomRoleParams struct {
|
||||
type InsertCustomRoleParams struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
|
||||
@ -6589,8 +6582,8 @@ type UpsertCustomRoleParams struct {
|
||||
UserPermissions CustomRolePermissions `db:"user_permissions" json:"user_permissions"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleParams) (CustomRole, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertCustomRole,
|
||||
func (q *sqlQuerier) InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertCustomRole,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.OrganizationID,
|
||||
@ -6613,6 +6606,54 @@ func (q *sqlQuerier) UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleP
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateCustomRole = `-- name: UpdateCustomRole :one
|
||||
UPDATE
|
||||
custom_roles
|
||||
SET
|
||||
display_name = $1,
|
||||
site_permissions = $2,
|
||||
org_permissions = $3,
|
||||
user_permissions = $4,
|
||||
updated_at = now()
|
||||
WHERE
|
||||
name = lower($5)
|
||||
AND organization_id = $6
|
||||
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id
|
||||
`
|
||||
|
||||
type UpdateCustomRoleParams struct {
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
SitePermissions CustomRolePermissions `db:"site_permissions" json:"site_permissions"`
|
||||
OrgPermissions CustomRolePermissions `db:"org_permissions" json:"org_permissions"`
|
||||
UserPermissions CustomRolePermissions `db:"user_permissions" json:"user_permissions"`
|
||||
Name string `db:"name" json:"name"`
|
||||
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateCustomRole,
|
||||
arg.DisplayName,
|
||||
arg.SitePermissions,
|
||||
arg.OrgPermissions,
|
||||
arg.UserPermissions,
|
||||
arg.Name,
|
||||
arg.OrganizationID,
|
||||
)
|
||||
var i CustomRole
|
||||
err := row.Scan(
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.SitePermissions,
|
||||
&i.OrgPermissions,
|
||||
&i.UserPermissions,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAnnouncementBanners = `-- name: GetAnnouncementBanners :one
|
||||
SELECT value FROM site_configs WHERE key = 'announcement_banners'
|
||||
`
|
||||
|
@ -33,7 +33,7 @@ WHERE
|
||||
AND organization_id = @organization_id
|
||||
;
|
||||
|
||||
-- name: UpsertCustomRole :one
|
||||
-- name: InsertCustomRole :one
|
||||
INSERT INTO
|
||||
custom_roles (
|
||||
name,
|
||||
@ -56,12 +56,18 @@ VALUES (
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE SET
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateCustomRole :one
|
||||
UPDATE
|
||||
custom_roles
|
||||
SET
|
||||
display_name = @display_name,
|
||||
site_permissions = @site_permissions,
|
||||
org_permissions = @org_permissions,
|
||||
user_permissions = @user_permissions,
|
||||
updated_at = now()
|
||||
RETURNING *
|
||||
;
|
||||
WHERE
|
||||
name = lower(@name)
|
||||
AND organization_id = @organization_id
|
||||
RETURNING *;
|
||||
|
@ -9,7 +9,7 @@ const (
|
||||
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
|
||||
UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
|
||||
UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
|
||||
UniqueCustomRolesPkey UniqueConstraint = "custom_roles_pkey" // ALTER TABLE ONLY custom_roles ADD CONSTRAINT custom_roles_pkey PRIMARY KEY (name);
|
||||
UniqueCustomRolesUniqueKey UniqueConstraint = "custom_roles_unique_key" // ALTER TABLE ONLY custom_roles ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id);
|
||||
UniqueDbcryptKeysActiveKeyDigestKey UniqueConstraint = "dbcrypt_keys_active_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_active_key_digest_key UNIQUE (active_key_digest);
|
||||
UniqueDbcryptKeysPkey UniqueConstraint = "dbcrypt_keys_pkey" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_pkey PRIMARY KEY (number);
|
||||
UniqueDbcryptKeysRevokedKeyDigestKey UniqueConstraint = "dbcrypt_keys_revoked_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_revoked_key_digest_key UNIQUE (revoked_key_digest);
|
||||
|
@ -28,9 +28,10 @@ var (
|
||||
// ResourceAssignOrgRole
|
||||
// Valid Actions
|
||||
// - "ActionAssign" :: ability to assign org scoped roles
|
||||
// - "ActionCreate" :: ability to create/delete/edit custom roles within an organization
|
||||
// - "ActionCreate" :: ability to create/delete custom roles within an organization
|
||||
// - "ActionDelete" :: ability to delete org scoped roles
|
||||
// - "ActionRead" :: view what roles are assignable
|
||||
// - "ActionUpdate" :: ability to edit custom roles within an organization
|
||||
ResourceAssignOrgRole = Object{
|
||||
Type: "assign_org_role",
|
||||
}
|
||||
@ -41,6 +42,7 @@ var (
|
||||
// - "ActionCreate" :: ability to create/delete/edit custom roles
|
||||
// - "ActionDelete" :: ability to unassign roles
|
||||
// - "ActionRead" :: view what roles are assignable
|
||||
// - "ActionUpdate" :: ability to edit custom roles
|
||||
ResourceAssignRole = Object{
|
||||
Type: "assign_role",
|
||||
}
|
||||
|
@ -227,6 +227,7 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
ActionRead: actDef("view what roles are assignable"),
|
||||
ActionDelete: actDef("ability to unassign roles"),
|
||||
ActionCreate: actDef("ability to create/delete/edit custom roles"),
|
||||
ActionUpdate: actDef("ability to edit custom roles"),
|
||||
},
|
||||
},
|
||||
"assign_org_role": {
|
||||
@ -234,7 +235,8 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
ActionAssign: actDef("ability to assign org scoped roles"),
|
||||
ActionRead: actDef("view what roles are assignable"),
|
||||
ActionDelete: actDef("ability to delete org scoped roles"),
|
||||
ActionCreate: actDef("ability to create/delete/edit custom roles within an organization"),
|
||||
ActionCreate: actDef("ability to create/delete custom roles within an organization"),
|
||||
ActionUpdate: actDef("ability to edit custom roles within an organization"),
|
||||
},
|
||||
},
|
||||
"oauth2_app": {
|
||||
|
@ -281,7 +281,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "CreateCustomRole",
|
||||
Actions: []policy.Action{policy.ActionCreate},
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceAssignRole,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
@ -317,7 +317,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "CreateOrgRoleAssignment",
|
||||
Actions: []policy.Action{policy.ActionCreate},
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
|
@ -58,8 +58,8 @@ const (
|
||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceWildcard: {},
|
||||
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceAuditLog: {ActionCreate, ActionRead},
|
||||
ResourceDebugInfo: {ActionRead},
|
||||
ResourceDeploymentConfig: {ActionRead, ActionUpdate},
|
||||
|
@ -61,8 +61,8 @@ type Role struct {
|
||||
UserPermissions []Permission `json:"user_permissions" table:"user_permissions"`
|
||||
}
|
||||
|
||||
// PatchRoleRequest is used to edit custom roles.
|
||||
type PatchRoleRequest struct {
|
||||
// CustomRoleRequest is used to edit custom roles.
|
||||
type CustomRoleRequest struct {
|
||||
Name string `json:"name" table:"name,default_sort" validate:"username"`
|
||||
DisplayName string `json:"display_name" table:"display_name"`
|
||||
SitePermissions []Permission `json:"site_permissions" table:"site_permissions"`
|
||||
@ -82,9 +82,9 @@ func (r Role) FullName() string {
|
||||
return r.Name + ":" + r.OrganizationID
|
||||
}
|
||||
|
||||
// PatchOrganizationRole will upsert a custom organization role
|
||||
func (c *Client) PatchOrganizationRole(ctx context.Context, role Role) (Role, error) {
|
||||
req := PatchRoleRequest{
|
||||
// CreateOrganizationRole will create a custom organization role
|
||||
func (c *Client) CreateOrganizationRole(ctx context.Context, role Role) (Role, error) {
|
||||
req := CustomRoleRequest{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: role.SitePermissions,
|
||||
@ -92,7 +92,30 @@ func (c *Client) PatchOrganizationRole(ctx context.Context, role Role) (Role, er
|
||||
UserPermissions: role.UserPermissions,
|
||||
}
|
||||
|
||||
res, err := c.Request(ctx, http.MethodPatch,
|
||||
res, err := c.Request(ctx, http.MethodPost,
|
||||
fmt.Sprintf("/api/v2/organizations/%s/members/roles", role.OrganizationID), req)
|
||||
if err != nil {
|
||||
return Role{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return Role{}, ReadBodyAsError(res)
|
||||
}
|
||||
var r Role
|
||||
return r, json.NewDecoder(res.Body).Decode(&r)
|
||||
}
|
||||
|
||||
// UpdateOrganizationRole will update an existing custom organization role
|
||||
func (c *Client) UpdateOrganizationRole(ctx context.Context, role Role) (Role, error) {
|
||||
req := CustomRoleRequest{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: role.SitePermissions,
|
||||
OrganizationPermissions: role.OrganizationPermissions,
|
||||
UserPermissions: role.UserPermissions,
|
||||
}
|
||||
|
||||
res, err := c.Request(ctx, http.MethodPut,
|
||||
fmt.Sprintf("/api/v2/organizations/%s/members/roles", role.OrganizationID), req)
|
||||
if err != nil {
|
||||
return Role{}, err
|
||||
|
166
docs/reference/api/members.md
generated
166
docs/reference/api/members.md
generated
@ -217,13 +217,13 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/members/roles \
|
||||
curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members/roles \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PATCH /organizations/{organization}/members/roles`
|
||||
`PUT /organizations/{organization}/members/roles`
|
||||
|
||||
> Body parameter
|
||||
|
||||
@ -258,9 +258,9 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/membe
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| -------------- | ---- | ---------------------------------------------------------------- | -------- | ------------------- |
|
||||
| -------------- | ---- | ------------------------------------------------------------------ | -------- | ------------------- |
|
||||
| `organization` | path | string(uuid) | true | Organization ID |
|
||||
| `body` | body | [codersdk.PatchRoleRequest](schemas.md#codersdkpatchrolerequest) | true | Upsert role request |
|
||||
| `body` | body | [codersdk.CustomRoleRequest](schemas.md#codersdkcustomrolerequest) | true | Upsert role request |
|
||||
|
||||
### Example responses
|
||||
|
||||
@ -369,6 +369,164 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Insert a custom organization role
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/members/roles \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /organizations/{organization}/members/roles`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"user_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------------ | -------- | ------------------- |
|
||||
| `organization` | path | string(uuid) | true | Organization ID |
|
||||
| `body` | body | [codersdk.CustomRoleRequest](schemas.md#codersdkcustomrolerequest) | true | Insert role request |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"user_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 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) |
|
||||
|
||||
<h3 id="insert-a-custom-organization-role-responseschema">Response Schema</h3>
|
||||
|
||||
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` | 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
|
||||
|
||||
| Property | Value |
|
||||
| --------------- | ------------------------- |
|
||||
| `action` | `application_connect` |
|
||||
| `action` | `assign` |
|
||||
| `action` | `create` |
|
||||
| `action` | `delete` |
|
||||
| `action` | `read` |
|
||||
| `action` | `read_personal` |
|
||||
| `action` | `ssh` |
|
||||
| `action` | `update` |
|
||||
| `action` | `update_personal` |
|
||||
| `action` | `use` |
|
||||
| `action` | `view_insights` |
|
||||
| `action` | `start` |
|
||||
| `action` | `stop` |
|
||||
| `resource_type` | `*` |
|
||||
| `resource_type` | `api_key` |
|
||||
| `resource_type` | `assign_org_role` |
|
||||
| `resource_type` | `assign_role` |
|
||||
| `resource_type` | `audit_log` |
|
||||
| `resource_type` | `debug_info` |
|
||||
| `resource_type` | `deployment_config` |
|
||||
| `resource_type` | `deployment_stats` |
|
||||
| `resource_type` | `file` |
|
||||
| `resource_type` | `group` |
|
||||
| `resource_type` | `group_member` |
|
||||
| `resource_type` | `license` |
|
||||
| `resource_type` | `notification_preference` |
|
||||
| `resource_type` | `notification_template` |
|
||||
| `resource_type` | `oauth2_app` |
|
||||
| `resource_type` | `oauth2_app_code_token` |
|
||||
| `resource_type` | `oauth2_app_secret` |
|
||||
| `resource_type` | `organization` |
|
||||
| `resource_type` | `organization_member` |
|
||||
| `resource_type` | `provisioner_daemon` |
|
||||
| `resource_type` | `provisioner_keys` |
|
||||
| `resource_type` | `replicas` |
|
||||
| `resource_type` | `system` |
|
||||
| `resource_type` | `tailnet_coordinator` |
|
||||
| `resource_type` | `template` |
|
||||
| `resource_type` | `user` |
|
||||
| `resource_type` | `workspace` |
|
||||
| `resource_type` | `workspace_dormant` |
|
||||
| `resource_type` | `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Delete a custom organization role
|
||||
|
||||
### Code samples
|
||||
|
80
docs/reference/api/schemas.md
generated
80
docs/reference/api/schemas.md
generated
@ -1400,6 +1400,46 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `template_version_id` | string | false | | Template version ID can be used to specify a specific version of a template for creating the workspace. |
|
||||
| `ttl_ms` | integer | false | | |
|
||||
|
||||
## codersdk.CustomRoleRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"user_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------ |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific to the organization the role belongs to. |
|
||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
|
||||
## codersdk.DAUEntry
|
||||
|
||||
```json
|
||||
@ -3763,46 +3803,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `quota_allowance` | integer | false | | |
|
||||
| `remove_users` | array of string | false | | |
|
||||
|
||||
## codersdk.PatchRoleRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"user_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------ |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific to the organization the role belongs to. |
|
||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
|
||||
## codersdk.PatchTemplateVersionRequest
|
||||
|
||||
```json
|
||||
|
@ -94,7 +94,7 @@ func TestEnterpriseListOrganizationMembers(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
//nolint:gocritic // only owners can patch roles
|
||||
customRole, err := ownerClient.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
customRole, err := ownerClient.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "custom",
|
||||
OrganizationID: owner.OrganizationID.String(),
|
||||
DisplayName: "Custom Role",
|
||||
@ -147,7 +147,7 @@ func TestAssignOrganizationMemberRole(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
// nolint:gocritic // requires owner role to create
|
||||
customRole, err := ownerClient.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
customRole, err := ownerClient.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "custom-role",
|
||||
OrganizationID: owner.OrganizationID.String(),
|
||||
DisplayName: "Custom Role",
|
||||
|
@ -168,7 +168,7 @@ func TestTemplateCreate(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner required to make custom roles
|
||||
orgTemplateAdminRole, err := ownerClient.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
orgTemplateAdminRole, err := ownerClient.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "org-template-admin",
|
||||
OrganizationID: secondOrg.ID.String(),
|
||||
OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
|
@ -269,7 +269,8 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentCustomRoles),
|
||||
httpmw.ExtractOrganizationParam(api.Database),
|
||||
)
|
||||
r.Patch("/organizations/{organization}/members/roles", api.patchOrgRoles)
|
||||
r.Post("/organizations/{organization}/members/roles", api.postOrgRoles)
|
||||
r.Put("/organizations/{organization}/members/roles", api.putOrgRoles)
|
||||
r.Delete("/organizations/{organization}/members/roles/{roleName}", api.deleteOrgRole)
|
||||
})
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@ -17,19 +18,19 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// patchRole will allow creating a custom organization role
|
||||
// postOrgRoles will allow creating a custom organization role
|
||||
//
|
||||
// @Summary Upsert a custom organization role
|
||||
// @ID upsert-a-custom-organization-role
|
||||
// @Summary Insert a custom organization role
|
||||
// @ID insert-a-custom-organization-role
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param request body codersdk.PatchRoleRequest true "Upsert role request"
|
||||
// @Param request body codersdk.CustomRoleRequest true "Insert role request"
|
||||
// @Tags Members
|
||||
// @Success 200 {array} codersdk.Role
|
||||
// @Router /organizations/{organization}/members/roles [patch]
|
||||
func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Router /organizations/{organization}/members/roles [post]
|
||||
func (api *API) postOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
db = api.Database
|
||||
@ -39,74 +40,22 @@ func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
Action: database.AuditActionCreate,
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
)
|
||||
defer commitAudit()
|
||||
|
||||
var req codersdk.PatchRoleRequest
|
||||
var req codersdk.CustomRoleRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// This check is not ideal, but we cannot enforce a unique role name in the db against
|
||||
// the built-in role names.
|
||||
if rbac.ReservedRoleName(req.Name) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Reserved role name",
|
||||
Detail: fmt.Sprintf("%q is a reserved role name, and not allowed to be used", req.Name),
|
||||
})
|
||||
if !validOrganizationRoleRequest(ctx, req, rw) {
|
||||
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
|
||||
}
|
||||
|
||||
// Only organization permissions are allowed to be granted
|
||||
if len(req.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
|
||||
}
|
||||
|
||||
if len(req.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
|
||||
}
|
||||
|
||||
originalRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{
|
||||
LookupRoles: []database.NameOrganizationPair{
|
||||
{
|
||||
Name: req.Name,
|
||||
OrganizationID: organization.ID,
|
||||
},
|
||||
},
|
||||
ExcludeOrgRoles: false,
|
||||
// Linter requires all fields to be set. This field is not actually required.
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
// If it is a 404 (not found) error, ignore it.
|
||||
if err != nil && !httpapi.Is404Error(err) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if len(originalRoles) == 1 {
|
||||
// For auditing changes to a role.
|
||||
aReq.Old = originalRoles[0]
|
||||
}
|
||||
|
||||
inserted, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
inserted, err := db.InsertCustomRole(ctx, database.InsertCustomRoleParams{
|
||||
Name: req.Name,
|
||||
DisplayName: req.DisplayName,
|
||||
OrganizationID: uuid.NullUUID{
|
||||
@ -133,6 +82,91 @@ func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(inserted))
|
||||
}
|
||||
|
||||
// patchRole will allow creating a custom organization role
|
||||
//
|
||||
// @Summary Upsert a custom organization role
|
||||
// @ID upsert-a-custom-organization-role
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param request body codersdk.CustomRoleRequest true "Upsert role request"
|
||||
// @Tags Members
|
||||
// @Success 200 {array} codersdk.Role
|
||||
// @Router /organizations/{organization}/members/roles [put]
|
||||
func (api *API) putOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
db = api.Database
|
||||
auditor = api.AGPL.Auditor.Load()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
aReq, commitAudit = audit.InitRequest[database.CustomRole](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
)
|
||||
defer commitAudit()
|
||||
|
||||
var req codersdk.CustomRoleRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if !validOrganizationRoleRequest(ctx, req, rw) {
|
||||
return
|
||||
}
|
||||
|
||||
originalRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{
|
||||
LookupRoles: []database.NameOrganizationPair{
|
||||
{
|
||||
Name: req.Name,
|
||||
OrganizationID: organization.ID,
|
||||
},
|
||||
},
|
||||
ExcludeOrgRoles: false,
|
||||
// Linter requires all fields to be set. This field is not actually required.
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
// If it is a 404 (not found) error, ignore it.
|
||||
if err != nil && !httpapi.Is404Error(err) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if len(originalRoles) == 1 {
|
||||
// For auditing changes to a role.
|
||||
aReq.Old = originalRoles[0]
|
||||
}
|
||||
|
||||
updated, err := db.UpdateCustomRole(ctx, database.UpdateCustomRoleParams{
|
||||
Name: req.Name,
|
||||
DisplayName: req.DisplayName,
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: organization.ID,
|
||||
Valid: true,
|
||||
},
|
||||
SitePermissions: db2sdk.List(req.SitePermissions, sdkPermissionToDB),
|
||||
OrgPermissions: db2sdk.List(req.OrganizationPermissions, sdkPermissionToDB),
|
||||
UserPermissions: db2sdk.List(req.UserPermissions, sdkPermissionToDB),
|
||||
})
|
||||
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
|
||||
}
|
||||
aReq.New = updated
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(updated))
|
||||
}
|
||||
|
||||
// deleteOrgRole will remove a custom role from an organization
|
||||
//
|
||||
// @Summary Delete a custom organization role
|
||||
@ -220,3 +254,42 @@ func sdkPermissionToDB(p codersdk.Permission) database.CustomRolePermission {
|
||||
Action: policy.Action(p.Action),
|
||||
}
|
||||
}
|
||||
|
||||
func validOrganizationRoleRequest(ctx context.Context, req codersdk.CustomRoleRequest, rw http.ResponseWriter) bool {
|
||||
// This check is not ideal, but we cannot enforce a unique role name in the db against
|
||||
// the built-in role names.
|
||||
if rbac.ReservedRoleName(req.Name) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Reserved role name",
|
||||
Detail: fmt.Sprintf("%q is a reserved role name, and not allowed to be used", req.Name),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if err := httpapi.NameValid(req.Name); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid role name",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Only organization permissions are allowed to be granted
|
||||
if len(req.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 false
|
||||
}
|
||||
|
||||
if len(req.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 false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err := owner.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
role, err := owner.CreateOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
// Assign the custom template admin role
|
||||
@ -111,7 +111,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err := owner.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
role, err := owner.CreateOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
// Remove the license to block enterprise functionality
|
||||
@ -124,7 +124,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify functionality is lost
|
||||
_, err = owner.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
_, err = owner.UpdateOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.ErrorContains(t, err, "Custom Roles is an Enterprise feature")
|
||||
|
||||
// Assign the custom template admin role
|
||||
@ -152,7 +152,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err := owner.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
role, err := owner.CreateOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
// Assign the custom template admin role
|
||||
@ -169,7 +169,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
newRole.SitePermissions = nil
|
||||
newRole.OrganizationPermissions = nil
|
||||
newRole.UserPermissions = nil
|
||||
_, err = owner.PatchOrganizationRole(ctx, newRole)
|
||||
_, err = owner.UpdateOrganizationRole(ctx, newRole)
|
||||
require.NoError(t, err, "upsert role with override")
|
||||
|
||||
// The role should no longer have template perms
|
||||
@ -203,7 +203,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
_, err := owner.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "Bad_Name", // No underscores allowed
|
||||
DisplayName: "Testing Purposes",
|
||||
OrganizationID: first.OrganizationID.String(),
|
||||
@ -232,7 +232,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
_, err := owner.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "owner", // Reserved
|
||||
DisplayName: "Testing Purposes",
|
||||
OrganizationID: first.OrganizationID.String(),
|
||||
@ -270,7 +270,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
}
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, siteRole)
|
||||
_, err := owner.CreateOrganizationRole(ctx, siteRole)
|
||||
require.ErrorContains(t, err, "site wide permissions")
|
||||
|
||||
userRole := templateAdminCustom(first.OrganizationID)
|
||||
@ -282,7 +282,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
}
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err = owner.PatchOrganizationRole(ctx, userRole)
|
||||
_, err = owner.UpdateOrganizationRole(ctx, userRole)
|
||||
require.ErrorContains(t, err, "not allowed to assign user permissions")
|
||||
})
|
||||
|
||||
@ -307,7 +307,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
newRole.OrganizationID = "0000" // This is not a valid uuid
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, newRole)
|
||||
_, err := owner.CreateOrganizationRole(ctx, newRole)
|
||||
require.ErrorContains(t, err, "Resource not found")
|
||||
})
|
||||
|
||||
@ -329,7 +329,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
orgAdmin, orgAdminUser := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
createdRole, err := orgAdmin.CreateOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
//nolint:gocritic // org_admin cannot assign to themselves
|
||||
@ -389,7 +389,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
orgAdmin, orgAdminUser := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
createdRole, err := orgAdmin.CreateOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
customRoleIdentifier := rbac.RoleIdentifier{
|
||||
|
@ -751,7 +751,7 @@ func TestTemplates(t *testing.T) {
|
||||
})
|
||||
|
||||
//nolint:gocritic // owner required to make custom roles
|
||||
orgTemplateAdminRole, err := ownerClient.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
orgTemplateAdminRole, err := ownerClient.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "org-template-admin",
|
||||
OrganizationID: secondOrg.ID.String(),
|
||||
OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
|
@ -705,7 +705,7 @@ func TestEnterpriseUserLogin(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
//nolint:gocritic // owner required
|
||||
customRole, err := ownerClient.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
customRole, err := ownerClient.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "custom-role",
|
||||
OrganizationID: owner.OrganizationID.String(),
|
||||
OrganizationPermissions: []codersdk.Permission{},
|
||||
|
@ -271,7 +271,7 @@ func TestAssignCustomOrgRoles(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
// Create a custom role as an organization admin that allows making templates.
|
||||
auditorRole, err := client.PatchOrganizationRole(ctx, codersdk.Role{
|
||||
auditorRole, err := client.CreateOrganizationRole(ctx, codersdk.Role{
|
||||
Name: "org-template-admin",
|
||||
OrganizationID: owner.OrganizationID.String(),
|
||||
DisplayName: "Template Admin",
|
||||
|
@ -603,11 +603,26 @@ class ApiMethods {
|
||||
/**
|
||||
* @param organization Can be the organization's ID or name
|
||||
*/
|
||||
patchOrganizationRole = async (
|
||||
createOrganizationRole = async (
|
||||
organization: string,
|
||||
role: TypesGen.Role,
|
||||
): Promise<TypesGen.Role> => {
|
||||
const response = await this.axios.patch<TypesGen.Role>(
|
||||
const response = await this.axios.post<TypesGen.Role>(
|
||||
`/api/v2/organizations/${organization}/members/roles`,
|
||||
role,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param organization Can be the organization's ID or name
|
||||
*/
|
||||
updateOrganizationRole = async (
|
||||
organization: string,
|
||||
role: TypesGen.Role,
|
||||
): Promise<TypesGen.Role> => {
|
||||
const response = await this.axios.put<TypesGen.Role>(
|
||||
`/api/v2/organizations/${organization}/members/roles`,
|
||||
role,
|
||||
);
|
||||
|
@ -23,13 +23,27 @@ export const organizationRoles = (organization: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const patchOrganizationRole = (
|
||||
export const createOrganizationRole = (
|
||||
queryClient: QueryClient,
|
||||
organization: string,
|
||||
) => {
|
||||
return {
|
||||
mutationFn: (request: Role) =>
|
||||
API.patchOrganizationRole(organization, request),
|
||||
API.createOrganizationRole(organization, request),
|
||||
onSuccess: async (updatedRole: Role) =>
|
||||
await queryClient.invalidateQueries(
|
||||
getRoleQueryKey(organization, updatedRole.name),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const updateOrganizationRole = (
|
||||
queryClient: QueryClient,
|
||||
organization: string,
|
||||
) => {
|
||||
return {
|
||||
mutationFn: (request: Role) =>
|
||||
API.updateOrganizationRole(organization, request),
|
||||
onSuccess: async (updatedRole: Role) =>
|
||||
await queryClient.invalidateQueries(
|
||||
getRoleQueryKey(organization, updatedRole.name),
|
||||
|
@ -16,15 +16,17 @@ export const RBACResourceActions: Partial<
|
||||
},
|
||||
assign_org_role: {
|
||||
assign: "ability to assign org scoped roles",
|
||||
create: "ability to create/delete/edit custom roles within an organization",
|
||||
create: "ability to create/delete custom roles within an organization",
|
||||
delete: "ability to delete org scoped roles",
|
||||
read: "view what roles are assignable",
|
||||
update: "ability to edit custom roles within an organization",
|
||||
},
|
||||
assign_role: {
|
||||
assign: "ability to assign roles",
|
||||
create: "ability to create/delete/edit custom roles",
|
||||
delete: "ability to unassign roles",
|
||||
read: "view what roles are assignable",
|
||||
update: "ability to edit custom roles",
|
||||
},
|
||||
audit_log: {
|
||||
create: "create new audit log entries",
|
||||
|
18
site/src/api/typesGenerated.ts
generated
18
site/src/api/typesGenerated.ts
generated
@ -345,6 +345,15 @@ export interface CreateWorkspaceRequest {
|
||||
readonly automatic_updates?: AutomaticUpdates;
|
||||
}
|
||||
|
||||
// From codersdk/roles.go
|
||||
export interface CustomRoleRequest {
|
||||
readonly name: string;
|
||||
readonly display_name: string;
|
||||
readonly site_permissions: readonly Permission[];
|
||||
readonly organization_permissions: readonly Permission[];
|
||||
readonly user_permissions: readonly Permission[];
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface DAUEntry {
|
||||
readonly date: string;
|
||||
@ -925,15 +934,6 @@ export interface PatchGroupRequest {
|
||||
readonly quota_allowance?: number;
|
||||
}
|
||||
|
||||
// From codersdk/roles.go
|
||||
export interface PatchRoleRequest {
|
||||
readonly name: string;
|
||||
readonly display_name: string;
|
||||
readonly site_permissions: readonly Permission[];
|
||||
readonly organization_permissions: readonly Permission[];
|
||||
readonly user_permissions: readonly Permission[];
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
export interface PatchTemplateVersionRequest {
|
||||
readonly name: string;
|
||||
|
@ -4,8 +4,12 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import { organizationPermissions } from "api/queries/organizations";
|
||||
import { patchOrganizationRole, organizationRoles } from "api/queries/roles";
|
||||
import type { PatchRoleRequest } from "api/typesGenerated";
|
||||
import {
|
||||
organizationRoles,
|
||||
createOrganizationRole,
|
||||
updateOrganizationRole,
|
||||
} from "api/queries/roles";
|
||||
import type { CustomRoleRequest } from "api/typesGenerated";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { pageTitle } from "utils/page";
|
||||
@ -22,8 +26,11 @@ export const CreateEditRolePage: FC = () => {
|
||||
const { organizations } = useOrganizationSettings();
|
||||
const organization = organizations?.find((o) => o.name === organizationName);
|
||||
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
|
||||
const patchOrganizationRoleMutation = useMutation(
|
||||
patchOrganizationRole(queryClient, organizationName),
|
||||
const createOrganizationRoleMutation = useMutation(
|
||||
createOrganizationRole(queryClient, organizationName),
|
||||
);
|
||||
const updateOrganizationRoleMutation = useMutation(
|
||||
updateOrganizationRole(queryClient, organizationName),
|
||||
);
|
||||
const { data: roleData, isLoading } = useQuery(
|
||||
organizationRoles(organizationName),
|
||||
@ -47,9 +54,13 @@ export const CreateEditRolePage: FC = () => {
|
||||
|
||||
<CreateEditRolePageView
|
||||
role={role}
|
||||
onSubmit={async (data: PatchRoleRequest) => {
|
||||
onSubmit={async (data: CustomRoleRequest) => {
|
||||
try {
|
||||
await patchOrganizationRoleMutation.mutateAsync(data);
|
||||
if (role) {
|
||||
await updateOrganizationRoleMutation.mutateAsync(data);
|
||||
} else {
|
||||
await createOrganizationRoleMutation.mutateAsync(data);
|
||||
}
|
||||
navigate(`/organizations/${organizationName}/roles`);
|
||||
} catch (error) {
|
||||
displayError(
|
||||
@ -57,8 +68,16 @@ export const CreateEditRolePage: FC = () => {
|
||||
);
|
||||
}
|
||||
}}
|
||||
error={patchOrganizationRoleMutation.error}
|
||||
isLoading={patchOrganizationRoleMutation.isLoading}
|
||||
error={
|
||||
role
|
||||
? updateOrganizationRoleMutation.error
|
||||
: createOrganizationRoleMutation.error
|
||||
}
|
||||
isLoading={
|
||||
role
|
||||
? updateOrganizationRoleMutation.isLoading
|
||||
: createOrganizationRoleMutation.isLoading
|
||||
}
|
||||
organizationName={organizationName}
|
||||
canAssignOrgRole={permissions.assignOrgRole}
|
||||
/>
|
||||
|
@ -20,7 +20,7 @@ import { isApiValidationError } from "api/errors";
|
||||
import { RBACResourceActions } from "api/rbacresources_gen";
|
||||
import type {
|
||||
Role,
|
||||
PatchRoleRequest,
|
||||
CustomRoleRequest,
|
||||
Permission,
|
||||
AssignableRoles,
|
||||
RBACResource,
|
||||
@ -38,7 +38,7 @@ const validationSchema = Yup.object({
|
||||
|
||||
export type CreateEditRolePageViewProps = {
|
||||
role: AssignableRoles | undefined;
|
||||
onSubmit: (data: PatchRoleRequest) => void;
|
||||
onSubmit: (data: CustomRoleRequest) => void;
|
||||
error?: unknown;
|
||||
isLoading: boolean;
|
||||
organizationName: string;
|
||||
@ -58,7 +58,7 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const onCancel = () => navigate(-1);
|
||||
|
||||
const form = useFormik<PatchRoleRequest>({
|
||||
const form = useFormik<CustomRoleRequest>({
|
||||
initialValues: {
|
||||
name: role?.name || "",
|
||||
display_name: role?.display_name || "",
|
||||
|
Reference in New Issue
Block a user