mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: add api for patching custom org roles (#13357)
* chore: implement patching custom organization roles
This commit is contained in:
84
coderd/apidoc/docs.go
generated
84
coderd/apidoc/docs.go
generated
@ -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": {
|
||||
|
76
coderd/apidoc/swagger.json
generated
76
coderd/apidoc/swagger.json
generated
@ -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": {
|
||||
|
@ -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,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
|
||||
|
Reference in New Issue
Block a user