mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add api for patching custom org roles (#13357)
* chore: implement patching custom organization roles
This commit is contained in:
Generated
+44
-40
@@ -2225,6 +2225,42 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Members"
|
||||
],
|
||||
"summary": "Upsert a custom organization role",
|
||||
"operationId": "upsert-a-custom-organization-role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/roles": {
|
||||
@@ -4362,32 +4398,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Members"
|
||||
],
|
||||
"summary": "Upsert a custom site-wide role",
|
||||
"operationId": "upsert-a-custom-site-wide-role",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}": {
|
||||
@@ -8426,13 +8436,10 @@ const docTemplate = `{
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
@@ -11250,13 +11257,10 @@ const docTemplate = `{
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
|
||||
Generated
+40
-36
@@ -1942,6 +1942,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Members"],
|
||||
"summary": "Upsert a custom organization role",
|
||||
"operationId": "upsert-a-custom-organization-role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/roles": {
|
||||
@@ -3841,28 +3873,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Members"],
|
||||
"summary": "Upsert a custom site-wide role",
|
||||
"operationId": "upsert-a-custom-site-wide-role",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}": {
|
||||
@@ -7481,13 +7491,10 @@
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
@@ -10142,13 +10149,10 @@
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
|
||||
+11
-1
@@ -424,6 +424,7 @@ func New(options *Options) *API {
|
||||
TemplateScheduleStore: options.TemplateScheduleStore,
|
||||
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
|
||||
AccessControlStore: options.AccessControlStore,
|
||||
CustomRoleHandler: atomic.Pointer[CustomRoleHandler]{},
|
||||
Experiments: experiments,
|
||||
healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{},
|
||||
Acquirer: provisionerdserver.NewAcquirer(
|
||||
@@ -436,6 +437,8 @@ func New(options *Options) *API {
|
||||
workspaceUsageTracker: options.WorkspaceUsageTracker,
|
||||
}
|
||||
|
||||
var customRoleHandler CustomRoleHandler = &agplCustomRoleHandler{}
|
||||
api.CustomRoleHandler.Store(&customRoleHandler)
|
||||
api.AppearanceFetcher.Store(&appearance.DefaultFetcher)
|
||||
api.PortSharer.Store(&portsharing.DefaultPortSharer)
|
||||
buildInfo := codersdk.BuildInfoResponse{
|
||||
@@ -828,7 +831,12 @@ func New(options *Options) *API {
|
||||
})
|
||||
})
|
||||
r.Route("/members", func(r chi.Router) {
|
||||
r.Get("/roles", api.assignableOrgRoles)
|
||||
r.Route("/roles", func(r chi.Router) {
|
||||
r.Get("/", api.assignableOrgRoles)
|
||||
r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)).
|
||||
Patch("/", api.patchOrgRoles)
|
||||
})
|
||||
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractOrganizationMemberParam(options.Database),
|
||||
@@ -1249,6 +1257,8 @@ type API struct {
|
||||
// passed to dbauthz.
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
PortSharer atomic.Pointer[portsharing.PortSharer]
|
||||
// CustomRoleHandler is the AGPL/Enterprise implementation for custom roles.
|
||||
CustomRoleHandler atomic.Pointer[CustomRoleHandler]
|
||||
|
||||
HTTPAuth *HTTPAuthorizer
|
||||
|
||||
|
||||
@@ -531,12 +531,16 @@ func Role(role rbac.Role) codersdk.Role {
|
||||
if err != nil {
|
||||
roleName = role.Name
|
||||
}
|
||||
|
||||
return codersdk.Role{
|
||||
Name: roleName,
|
||||
OrganizationID: orgIDStr,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: List(role.Site, Permission),
|
||||
OrganizationPermissions: Map(role.Org, ListLazy(Permission)),
|
||||
Name: roleName,
|
||||
OrganizationID: orgIDStr,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: List(role.Site, Permission),
|
||||
// This is not perfect. If there are organization permissions in another
|
||||
// organization, they will be omitted. This should not be allowed, so
|
||||
// should never happen.
|
||||
OrganizationPermissions: List(role.Org[orgIDStr], Permission),
|
||||
UserPermissions: List(role.User, Permission),
|
||||
}
|
||||
}
|
||||
@@ -550,11 +554,18 @@ func Permission(permission rbac.Permission) codersdk.Permission {
|
||||
}
|
||||
|
||||
func RoleToRBAC(role codersdk.Role) rbac.Role {
|
||||
orgPerms := map[string][]rbac.Permission{}
|
||||
if role.OrganizationID != "" {
|
||||
orgPerms = map[string][]rbac.Permission{
|
||||
role.OrganizationID: List(role.OrganizationPermissions, PermissionToRBAC),
|
||||
}
|
||||
}
|
||||
|
||||
return rbac.Role{
|
||||
Name: rbac.RoleName(role.Name, role.OrganizationID),
|
||||
DisplayName: role.DisplayName,
|
||||
Site: List(role.SitePermissions, PermissionToRBAC),
|
||||
Org: Map(role.OrganizationPermissions, ListLazy(PermissionToRBAC)),
|
||||
Org: orgPerms,
|
||||
User: List(role.UserPermissions, PermissionToRBAC),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
|
||||
customRoles := make([]string, 0)
|
||||
// Validate that the roles being assigned are valid.
|
||||
for _, r := range grantedRoles {
|
||||
_, isOrgRole := rbac.IsOrgRole(r)
|
||||
roleOrgIDStr, isOrgRole := rbac.IsOrgRole(r)
|
||||
if shouldBeOrgRoles && !isOrgRole {
|
||||
return xerrors.Errorf("Must only update org roles")
|
||||
}
|
||||
@@ -608,6 +608,21 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
|
||||
return xerrors.Errorf("Must only update site wide roles")
|
||||
}
|
||||
|
||||
if shouldBeOrgRoles {
|
||||
roleOrgID, err := uuid.Parse(roleOrgIDStr)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("role %q has invalid uuid for org: %w", r, err)
|
||||
}
|
||||
|
||||
if orgID == nil {
|
||||
return xerrors.Errorf("should never happen, orgID is nil, but trying to assign an organization role")
|
||||
}
|
||||
|
||||
if roleOrgID != *orgID {
|
||||
return xerrors.Errorf("attempted to assign role from a different org, role %q to %q", r, orgID.String())
|
||||
}
|
||||
}
|
||||
|
||||
// All roles should be valid roles
|
||||
if _, err := rbac.RoleByName(r); err != nil {
|
||||
customRoles = append(customRoles, r)
|
||||
|
||||
+1
-36
@@ -1,13 +1,8 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
|
||||
@@ -48,7 +43,7 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := api.updateOrganizationMemberRoles(ctx, database.UpdateMemberRolesParams{
|
||||
updatedUser, err := api.Database.UpdateMemberRoles(ctx, database.UpdateMemberRolesParams{
|
||||
GrantedRoles: params.Roles,
|
||||
UserID: member.UserID,
|
||||
OrgID: organization.ID,
|
||||
@@ -63,36 +58,6 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertOrganizationMember(updatedUser))
|
||||
}
|
||||
|
||||
func (api *API) updateOrganizationMemberRoles(ctx context.Context, args database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
// Enforce only site wide roles
|
||||
for _, r := range args.GrantedRoles {
|
||||
// Must be an org role for the org in the args
|
||||
orgID, ok := rbac.IsOrgRole(r)
|
||||
if !ok {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("must only update organization roles")
|
||||
}
|
||||
|
||||
roleOrg, err := uuid.Parse(orgID)
|
||||
if err != nil {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("Role must have proper UUIDs for organization, %q does not", r)
|
||||
}
|
||||
|
||||
if roleOrg != args.OrgID {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("Must only pass roles for org %q", args.OrgID.String())
|
||||
}
|
||||
|
||||
if _, err := rbac.RoleByName(r); err != nil {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("%q is not a supported organization role", r)
|
||||
}
|
||||
}
|
||||
|
||||
updatedUser, err := api.Database.UpdateMemberRoles(ctx, args)
|
||||
if err != nil {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("Update site roles: %w", err)
|
||||
}
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
func convertOrganizationMember(mem database.OrganizationMember) codersdk.OrganizationMember {
|
||||
convertedMember := codersdk.OrganizationMember{
|
||||
UserID: mem.UserID,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -16,6 +17,52 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
)
|
||||
|
||||
// CustomRoleHandler handles AGPL/Enterprise interface for handling custom
|
||||
// roles. Ideally only included in the enterprise package, but the routes are
|
||||
// intermixed with AGPL endpoints.
|
||||
type CustomRoleHandler interface {
|
||||
PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool)
|
||||
}
|
||||
|
||||
type agplCustomRoleHandler struct{}
|
||||
|
||||
func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, _ database.Store, rw http.ResponseWriter, _ uuid.UUID, _ codersdk.Role) (codersdk.Role, bool) {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Creating and updating custom roles is an Enterprise feature. Contact sales!",
|
||||
})
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
// patchRole will allow creating a custom organization role
|
||||
//
|
||||
// @Summary Upsert a custom organization role
|
||||
// @ID upsert-a-custom-organization-role
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Tags Members
|
||||
// @Success 200 {array} codersdk.Role
|
||||
// @Router /organizations/{organization}/members/roles [patch]
|
||||
func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
handler = *api.CustomRoleHandler.Load()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
)
|
||||
|
||||
var req codersdk.Role
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
updated, ok := handler.PatchOrganizationRole(ctx, api.Database, rw, organization.ID, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
// AssignableSiteRoles returns all site wide roles that can be assigned.
|
||||
//
|
||||
// @Summary Get site member roles
|
||||
|
||||
+18
-6
@@ -39,14 +39,26 @@ type Role struct {
|
||||
OrganizationID string `json:"organization_id" table:"organization_id" format:"uuid"`
|
||||
DisplayName string `json:"display_name" table:"display_name"`
|
||||
SitePermissions []Permission `json:"site_permissions" table:"site_permissions"`
|
||||
// map[<org_id>] -> Permissions
|
||||
OrganizationPermissions map[string][]Permission `json:"organization_permissions" table:"org_permissions"`
|
||||
UserPermissions []Permission `json:"user_permissions" table:"user_permissions"`
|
||||
// OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.
|
||||
OrganizationPermissions []Permission `json:"organization_permissions" table:"org_permissions"`
|
||||
UserPermissions []Permission `json:"user_permissions" table:"user_permissions"`
|
||||
}
|
||||
|
||||
// PatchRole will upsert a custom site wide role
|
||||
func (c *Client) PatchRole(ctx context.Context, req Role) (Role, error) {
|
||||
res, err := c.Request(ctx, http.MethodPatch, "/api/v2/users/roles", req)
|
||||
// FullName returns the role name scoped to the organization ID. This is useful if
|
||||
// printing a set of roles from different scopes, as duplicated names across multiple
|
||||
// scopes will become unique.
|
||||
// In practice, this is primarily used in testing.
|
||||
func (r Role) FullName() string {
|
||||
if r.OrganizationID == "" {
|
||||
return r.Name
|
||||
}
|
||||
return r.Name + ":" + r.OrganizationID
|
||||
}
|
||||
|
||||
// PatchOrganizationRole will upsert a custom organization role
|
||||
func (c *Client) PatchOrganizationRole(ctx context.Context, organizationID uuid.UUID, req Role) (Role, error) {
|
||||
res, err := c.Request(ctx, http.MethodPatch,
|
||||
fmt.Sprintf("/api/v2/organizations/%s/members/roles", organizationID.String()), req)
|
||||
if err != nil {
|
||||
return Role{}, err
|
||||
}
|
||||
|
||||
Generated
+164
-188
@@ -31,22 +31,13 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_permissions": {
|
||||
"property1": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"property2": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
@@ -75,21 +66,142 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» assignable` | boolean | false | | |
|
||||
| `» built_in` | boolean | false | | Built in roles are immutable |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||
| `»» [any property]` | array | false | | |
|
||||
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||
| `»»» negate` | boolean | false | | Negate makes this a negative permission |
|
||||
| `»»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
|
||||
| `» site_permissions` | array | false | | |
|
||||
| `» user_permissions` | array | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» assignable` | boolean | false | | |
|
||||
| `» built_in` | boolean | false | | Built in roles are immutable |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
|
||||
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||
| `»» negate` | boolean | false | | Negate makes this a negative permission |
|
||||
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
|
||||
| `» site_permissions` | array | false | | |
|
||||
| `» user_permissions` | array | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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` | `license` |
|
||||
| `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` | `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).
|
||||
|
||||
## Upsert a custom organization role
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/members/roles \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PATCH /organizations/{organization}/members/roles`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| -------------- | ---- | ------------ | -------- | --------------- |
|
||||
| `organization` | path | string(uuid) | true | Organization ID |
|
||||
|
||||
### 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="upsert-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
|
||||
|
||||
@@ -218,22 +330,13 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_permissions": {
|
||||
"property1": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"property2": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
@@ -262,147 +365,20 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» assignable` | boolean | false | | |
|
||||
| `» built_in` | boolean | false | | Built in roles are immutable |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||
| `»» [any property]` | array | false | | |
|
||||
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||
| `»»» negate` | boolean | false | | Negate makes this a negative permission |
|
||||
| `»»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
|
||||
| `» site_permissions` | array | false | | |
|
||||
| `» user_permissions` | array | false | | |
|
||||
|
||||
#### 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` | `license` |
|
||||
| `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` | `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).
|
||||
|
||||
## Upsert a custom site-wide role
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PATCH http://coder-server:8080/api/v2/users/roles \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PATCH /users/roles`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_permissions": {
|
||||
"property1": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"property2": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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="upsert-a-custom-site-wide-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` | object | false | | map[<org_id>] -> Permissions |
|
||||
| `»» [any property]` | array | false | | |
|
||||
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||
| `»»» negate` | boolean | false | | Negate makes this a negative permission |
|
||||
| `»»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
|
||||
| `» site_permissions` | array | false | | |
|
||||
| `» user_permissions` | array | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------------- | -------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» assignable` | boolean | false | | |
|
||||
| `» built_in` | boolean | false | | Built in roles are immutable |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
|
||||
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||
| `»» negate` | boolean | false | | Negate makes this a negative permission |
|
||||
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
|
||||
| `» site_permissions` | array | false | | |
|
||||
| `» user_permissions` | array | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
|
||||
Generated
+32
-52
@@ -806,22 +806,13 @@
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_permissions": {
|
||||
"property1": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"property2": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
@@ -841,17 +832,16 @@
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- |
|
||||
| `assignable` | boolean | false | | |
|
||||
| `built_in` | boolean | false | | Built in roles are immutable |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||
| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `assignable` | boolean | false | | |
|
||||
| `built_in` | boolean | false | | Built in roles are immutable |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
|
||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
|
||||
## codersdk.AuditAction
|
||||
|
||||
@@ -4330,22 +4320,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_permissions": {
|
||||
"property1": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"property2": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"organization_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
"negate": true,
|
||||
"resource_type": "*"
|
||||
}
|
||||
],
|
||||
"site_permissions": [
|
||||
{
|
||||
"action": "application_connect",
|
||||
@@ -4365,15 +4346,14 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||
| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `display_name` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
|
||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||
|
||||
## codersdk.SSHConfig
|
||||
|
||||
|
||||
@@ -327,22 +327,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/users/roles", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(
|
||||
api.customRolesEnabledMW,
|
||||
)
|
||||
r.Patch("/", api.patchRole)
|
||||
})
|
||||
// Unfortunate, but this r.Route overrides the AGPL roles route.
|
||||
// The AGPL does not have the entitlements to block the licensed
|
||||
// routes, so we need to duplicate the AGPL here.
|
||||
r.Get("/", api.AGPL.AssignableSiteRoles)
|
||||
})
|
||||
|
||||
r.Route("/users/{user}/quiet-hours", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.autostopRequirementEnabledMW,
|
||||
@@ -761,6 +745,11 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
api.AGPL.PortSharer.Store(&ps)
|
||||
}
|
||||
|
||||
if initial, changed, enabled := featureChanged(codersdk.FeatureCustomRoles); shouldUpdate(initial, changed, enabled) {
|
||||
var handler coderd.CustomRoleHandler = &enterpriseCustomRoleHandler{Enabled: enabled}
|
||||
api.AGPL.CustomRoleHandler.Store(&handler)
|
||||
}
|
||||
|
||||
// External token encryption is soft-enforced
|
||||
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
||||
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
|
||||
|
||||
+47
-32
@@ -1,6 +1,8 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -12,70 +14,83 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// patchRole will allow creating a custom role
|
||||
//
|
||||
// @Summary Upsert a custom site-wide role
|
||||
// @ID upsert-a-custom-site-wide-role
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Members
|
||||
// @Success 200 {array} codersdk.Role
|
||||
// @Router /users/roles [patch]
|
||||
func (api *API) patchRole(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
type enterpriseCustomRoleHandler struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
var req codersdk.Role
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) {
|
||||
if !h.Enabled {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Custom roles are not enabled",
|
||||
})
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
if err := httpapi.NameValid(req.Name); err != nil {
|
||||
if err := httpapi.NameValid(role.Name); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid role name",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
if len(req.OrganizationPermissions) > 0 {
|
||||
// Org perms should be assigned only in org specific roles. Otherwise,
|
||||
// it gets complicated to keep track of who can do what.
|
||||
// Only organization permissions are allowed to be granted
|
||||
if len(role.SitePermissions) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid request, not allowed to assign organization permissions for a site wide role.",
|
||||
Detail: "site wide roles may not contain organization specific permissions",
|
||||
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
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
if len(role.UserPermissions) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid request, not allowed to assign user permissions for an organization role.",
|
||||
Detail: "organization scoped roles may not contain user permissions",
|
||||
})
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
if role.OrganizationID != orgID.String() {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid request, organization in role and url must match",
|
||||
Detail: fmt.Sprintf("role organization=%q does not match URL=%q", role.OrganizationID, orgID.String()),
|
||||
})
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
// Make sure all permissions inputted are valid according to our policy.
|
||||
rbacRole := db2sdk.RoleToRBAC(req)
|
||||
rbacRole := db2sdk.RoleToRBAC(role)
|
||||
args, err := rolestore.ConvertRoleToDB(rbacRole)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid request",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
Name: args.Name,
|
||||
DisplayName: args.DisplayName,
|
||||
OrganizationID: uuid.NullUUID{},
|
||||
inserted, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
Name: args.Name,
|
||||
DisplayName: args.DisplayName,
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: orgID,
|
||||
Valid: true,
|
||||
},
|
||||
SitePermissions: args.SitePermissions,
|
||||
OrgPermissions: args.OrgPermissions,
|
||||
UserPermissions: args.UserPermissions,
|
||||
})
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to update role permissions",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
convertedInsert, err := rolestore.ConvertDBRole(inserted)
|
||||
@@ -84,8 +99,8 @@ func (api *API) patchRole(rw http.ResponseWriter, r *http.Request) {
|
||||
Message: "Permissions were updated, unable to read them back out of the database.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(convertedInsert))
|
||||
return db2sdk.Role(convertedInsert), true
|
||||
}
|
||||
|
||||
+145
-47
@@ -5,10 +5,10 @@ import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
@@ -16,19 +16,22 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestCustomRole(t *testing.T) {
|
||||
func TestCustomOrganizationRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
templateAdminCustom := codersdk.Role{
|
||||
Name: "test-role",
|
||||
DisplayName: "Testing Purposes",
|
||||
// Basically creating a template admin manually
|
||||
SitePermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights},
|
||||
codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead},
|
||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||
}),
|
||||
OrganizationPermissions: nil,
|
||||
UserPermissions: nil,
|
||||
templateAdminCustom := func(orgID uuid.UUID) codersdk.Role {
|
||||
return codersdk.Role{
|
||||
Name: "test-role",
|
||||
DisplayName: "Testing Purposes",
|
||||
OrganizationID: orgID.String(),
|
||||
// Basically creating a template admin manually
|
||||
SitePermissions: nil,
|
||||
OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights},
|
||||
codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead},
|
||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||
}),
|
||||
UserPermissions: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Create, assign, and use a custom role
|
||||
@@ -50,31 +53,43 @@ func TestCustomRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err := owner.PatchRole(ctx, templateAdminCustom)
|
||||
role, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
// Assign the custom template admin role
|
||||
tmplAdmin, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name)
|
||||
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName())
|
||||
|
||||
// Assert the role exists
|
||||
roleNamesF := func(role codersdk.SlimRole) string { return role.Name }
|
||||
require.Contains(t, db2sdk.List(user.Roles, roleNamesF), role.Name)
|
||||
// TODO: At present user roles are not returned by the user endpoints.
|
||||
// Changing this might mess up the UI in how it renders the roles on the
|
||||
// users page. When the users endpoint is updated, this should be uncommented.
|
||||
// roleNamesF := func(role codersdk.SlimRole) string { return role.Name }
|
||||
// require.Contains(t, db2sdk.List(user.Roles, roleNamesF), role.Name)
|
||||
|
||||
// Try to create a template version
|
||||
coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil)
|
||||
|
||||
// Verify the role exists in the list
|
||||
allRoles, err := tmplAdmin.ListSiteRoles(ctx)
|
||||
allRoles, err := tmplAdmin.ListOrganizationRoles(ctx, first.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundRole codersdk.AssignableRoles
|
||||
require.True(t, slices.ContainsFunc(allRoles, func(selected codersdk.AssignableRoles) bool {
|
||||
return selected.Name == role.Name
|
||||
}), "role missing from site role list")
|
||||
if selected.Name == role.Name {
|
||||
foundRole = selected
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), "role missing from org role list")
|
||||
|
||||
require.Len(t, foundRole.SitePermissions, 0)
|
||||
require.Len(t, foundRole.OrganizationPermissions, 7)
|
||||
require.Len(t, foundRole.UserPermissions, 0)
|
||||
})
|
||||
|
||||
// Revoked licenses cannot modify/create custom roles, but they can
|
||||
// use the existing roles.
|
||||
t.Run("Revoked License", func(t *testing.T) {
|
||||
t.Run("RevokedLicense", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
@@ -92,7 +107,7 @@ func TestCustomRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err := owner.PatchRole(ctx, templateAdminCustom)
|
||||
role, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
// Remove the license to block enterprise functionality
|
||||
@@ -105,11 +120,11 @@ func TestCustomRole(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify functionality is lost
|
||||
_, err = owner.PatchRole(ctx, templateAdminCustom)
|
||||
require.ErrorContains(t, err, "Custom roles is an Enterprise feature", "upsert role")
|
||||
_, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID))
|
||||
require.ErrorContains(t, err, "roles are not enabled")
|
||||
|
||||
// Assign the custom template admin role
|
||||
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name)
|
||||
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName())
|
||||
|
||||
// Try to create a template version, eg using the custom role
|
||||
coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil)
|
||||
@@ -133,26 +148,24 @@ func TestCustomRole(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err := owner.PatchRole(ctx, templateAdminCustom)
|
||||
role, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
// Assign the custom template admin role
|
||||
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name)
|
||||
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.FullName())
|
||||
|
||||
// Try to create a template version, eg using the custom role
|
||||
coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
role, err = owner.PatchRole(ctx, codersdk.Role{
|
||||
Name: templateAdminCustom.Name,
|
||||
DisplayName: templateAdminCustom.DisplayName,
|
||||
// These are all left nil, which sets the custom role to have 0
|
||||
// permissions. Omitting this does not "inherit" what already
|
||||
// exists.
|
||||
SitePermissions: nil,
|
||||
OrganizationPermissions: nil,
|
||||
UserPermissions: nil,
|
||||
})
|
||||
newRole := templateAdminCustom(first.OrganizationID)
|
||||
// These are all left nil, which sets the custom role to have 0
|
||||
// permissions. Omitting this does not "inherit" what already
|
||||
// exists.
|
||||
newRole.SitePermissions = nil
|
||||
newRole.OrganizationPermissions = nil
|
||||
newRole.UserPermissions = nil
|
||||
_, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, newRole)
|
||||
require.NoError(t, err, "upsert role with override")
|
||||
|
||||
// The role should no longer have template perms
|
||||
@@ -172,7 +185,7 @@ func TestCustomRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
owner, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
@@ -186,18 +199,103 @@ func TestCustomRole(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchRole(ctx, codersdk.Role{
|
||||
Name: "Bad_Name", // No underscores allowed
|
||||
DisplayName: "Testing Purposes",
|
||||
// Basically creating a template admin manually
|
||||
SitePermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||
codersdk.ResourceTemplate: {codersdk.ActionCreate, codersdk.ActionRead, codersdk.ActionUpdate, codersdk.ActionViewInsights},
|
||||
codersdk.ResourceFile: {codersdk.ActionCreate, codersdk.ActionRead},
|
||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||
}),
|
||||
_, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, codersdk.Role{
|
||||
Name: "Bad_Name", // No underscores allowed
|
||||
DisplayName: "Testing Purposes",
|
||||
SitePermissions: nil,
|
||||
OrganizationPermissions: nil,
|
||||
UserPermissions: nil,
|
||||
})
|
||||
require.ErrorContains(t, err, "Validation")
|
||||
})
|
||||
|
||||
t.Run("MismatchedOrganizations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(uuid.New()))
|
||||
require.ErrorContains(t, err, "does not match")
|
||||
})
|
||||
|
||||
// Attempt to add site & user permissions, which is not allowed
|
||||
t.Run("ExcessPermissions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
siteRole := templateAdminCustom(first.OrganizationID)
|
||||
siteRole.SitePermissions = []codersdk.Permission{
|
||||
{
|
||||
ResourceType: codersdk.ResourceWorkspace,
|
||||
Action: codersdk.ActionRead,
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, siteRole)
|
||||
require.ErrorContains(t, err, "site wide permissions")
|
||||
|
||||
userRole := templateAdminCustom(first.OrganizationID)
|
||||
userRole.UserPermissions = []codersdk.Permission{
|
||||
{
|
||||
ResourceType: codersdk.ResourceWorkspace,
|
||||
Action: codersdk.ActionRead,
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err = owner.PatchOrganizationRole(ctx, first.OrganizationID, userRole)
|
||||
require.ErrorContains(t, err, "not allowed to assign user permissions")
|
||||
})
|
||||
|
||||
t.Run("InvalidUUID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
newRole := templateAdminCustom(first.OrganizationID)
|
||||
newRole.OrganizationID = "0000" // This is not a valid uuid
|
||||
|
||||
//nolint:gocritic // owner is required for this
|
||||
_, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, newRole)
|
||||
require.ErrorContains(t, err, "Invalid request")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,31 +14,6 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func (api *API) customRolesEnabledMW(next http.Handler) http.Handler {
|
||||
return httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentCustomRoles)(
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Entitlement must be enabled.
|
||||
api.entitlementsMu.RLock()
|
||||
entitled := api.entitlements.Features[codersdk.FeatureCustomRoles].Entitlement != codersdk.EntitlementNotEntitled
|
||||
enabled := api.entitlements.Features[codersdk.FeatureCustomRoles].Enabled
|
||||
api.entitlementsMu.RUnlock()
|
||||
if !entitled {
|
||||
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Custom roles is an Enterprise feature. Contact sales!",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !enabled {
|
||||
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Custom roles is not enabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
}))
|
||||
}
|
||||
|
||||
func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Entitlement must be enabled.
|
||||
|
||||
Generated
+1
-1
@@ -980,7 +980,7 @@ export interface Role {
|
||||
readonly organization_id: string;
|
||||
readonly display_name: string;
|
||||
readonly site_permissions: readonly Permission[];
|
||||
readonly organization_permissions: Record<string, readonly Permission[]>;
|
||||
readonly organization_permissions: readonly Permission[];
|
||||
readonly user_permissions: readonly Permission[];
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ export const MockOwnerRole: TypesGen.Role = {
|
||||
name: "owner",
|
||||
display_name: "Owner",
|
||||
site_permissions: [],
|
||||
organization_permissions: {},
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: "",
|
||||
};
|
||||
@@ -242,7 +242,7 @@ export const MockUserAdminRole: TypesGen.Role = {
|
||||
name: "user_admin",
|
||||
display_name: "User Admin",
|
||||
site_permissions: [],
|
||||
organization_permissions: {},
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: "",
|
||||
};
|
||||
@@ -251,7 +251,7 @@ export const MockTemplateAdminRole: TypesGen.Role = {
|
||||
name: "template_admin",
|
||||
display_name: "Template Admin",
|
||||
site_permissions: [],
|
||||
organization_permissions: {},
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: "",
|
||||
};
|
||||
@@ -265,7 +265,7 @@ export const MockAuditorRole: TypesGen.Role = {
|
||||
name: "auditor",
|
||||
display_name: "Auditor",
|
||||
site_permissions: [],
|
||||
organization_permissions: {},
|
||||
organization_permissions: [],
|
||||
user_permissions: [],
|
||||
organization_id: "",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user