mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
chore: implement api for creating custom roles (#13298)
api endpoint (gated by experiment) to create custom_roles
This commit is contained in:
87
coderd/apidoc/docs.go
generated
87
coderd/apidoc/docs.go
generated
@ -4286,6 +4286,32 @@ 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}": {
|
"/users/{user}": {
|
||||||
@ -9547,17 +9573,20 @@ const docTemplate = `{
|
|||||||
"enum": [
|
"enum": [
|
||||||
"example",
|
"example",
|
||||||
"auto-fill-parameters",
|
"auto-fill-parameters",
|
||||||
"multi-organization"
|
"multi-organization",
|
||||||
|
"custom-roles"
|
||||||
],
|
],
|
||||||
"x-enum-comments": {
|
"x-enum-comments": {
|
||||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||||
|
"ExperimentCustomRoles": "Allows creating runtime custom roles",
|
||||||
"ExperimentExample": "This isn't used for anything.",
|
"ExperimentExample": "This isn't used for anything.",
|
||||||
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
||||||
},
|
},
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentExample",
|
"ExperimentExample",
|
||||||
"ExperimentAutoFillParameters",
|
"ExperimentAutoFillParameters",
|
||||||
"ExperimentMultiOrganization"
|
"ExperimentMultiOrganization",
|
||||||
|
"ExperimentCustomRoles"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.ExternalAuth": {
|
"codersdk.ExternalAuth": {
|
||||||
@ -10372,7 +10401,7 @@ const docTemplate = `{
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.Role"
|
"$ref": "#/definitions/codersdk.SlimRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
@ -10452,6 +10481,21 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.Permission": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"$ref": "#/definitions/codersdk.RBACAction"
|
||||||
|
},
|
||||||
|
"negate": {
|
||||||
|
"description": "Negate makes this a negative permission",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"resource_type": {
|
||||||
|
"$ref": "#/definitions/codersdk.RBACResource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.PostOAuth2ProviderAppRequest": {
|
"codersdk.PostOAuth2ProviderAppRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -11094,6 +11138,28 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"organization_permissions": {
|
||||||
|
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -11160,6 +11226,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.SlimRole": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.SupportConfig": {
|
"codersdk.SupportConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -11677,7 +11754,7 @@ const docTemplate = `{
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.Role"
|
"$ref": "#/definitions/codersdk.SlimRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@ -12214,7 +12291,7 @@ const docTemplate = `{
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.Role"
|
"$ref": "#/definitions/codersdk.SlimRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
87
coderd/apidoc/swagger.json
generated
87
coderd/apidoc/swagger.json
generated
@ -3775,6 +3775,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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}": {
|
"/users/{user}": {
|
||||||
@ -8545,16 +8567,23 @@
|
|||||||
},
|
},
|
||||||
"codersdk.Experiment": {
|
"codersdk.Experiment": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["example", "auto-fill-parameters", "multi-organization"],
|
"enum": [
|
||||||
|
"example",
|
||||||
|
"auto-fill-parameters",
|
||||||
|
"multi-organization",
|
||||||
|
"custom-roles"
|
||||||
|
],
|
||||||
"x-enum-comments": {
|
"x-enum-comments": {
|
||||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||||
|
"ExperimentCustomRoles": "Allows creating runtime custom roles",
|
||||||
"ExperimentExample": "This isn't used for anything.",
|
"ExperimentExample": "This isn't used for anything.",
|
||||||
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
||||||
},
|
},
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentExample",
|
"ExperimentExample",
|
||||||
"ExperimentAutoFillParameters",
|
"ExperimentAutoFillParameters",
|
||||||
"ExperimentMultiOrganization"
|
"ExperimentMultiOrganization",
|
||||||
|
"ExperimentCustomRoles"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.ExternalAuth": {
|
"codersdk.ExternalAuth": {
|
||||||
@ -9316,7 +9345,7 @@
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.Role"
|
"$ref": "#/definitions/codersdk.SlimRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
@ -9391,6 +9420,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.Permission": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"$ref": "#/definitions/codersdk.RBACAction"
|
||||||
|
},
|
||||||
|
"negate": {
|
||||||
|
"description": "Negate makes this a negative permission",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"resource_type": {
|
||||||
|
"$ref": "#/definitions/codersdk.RBACResource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.PostOAuth2ProviderAppRequest": {
|
"codersdk.PostOAuth2ProviderAppRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["callback_url", "name"],
|
"required": ["callback_url", "name"],
|
||||||
@ -9996,6 +10040,28 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"organization_permissions": {
|
||||||
|
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -10062,6 +10128,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.SlimRole": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.SupportConfig": {
|
"codersdk.SupportConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -10559,7 +10636,7 @@
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.Role"
|
"$ref": "#/definitions/codersdk.SlimRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@ -11053,7 +11130,7 @@
|
|||||||
"roles": {
|
"roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/codersdk.Role"
|
"$ref": "#/definitions/codersdk.SlimRole"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
@ -196,12 +196,12 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
|||||||
CreatedAt: dblog.UserCreatedAt.Time,
|
CreatedAt: dblog.UserCreatedAt.Time,
|
||||||
Status: codersdk.UserStatus(dblog.UserStatus.UserStatus),
|
Status: codersdk.UserStatus(dblog.UserStatus.UserStatus),
|
||||||
},
|
},
|
||||||
Roles: []codersdk.Role{},
|
Roles: []codersdk.SlimRole{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, roleName := range dblog.UserRoles {
|
for _, roleName := range dblog.UserRoles {
|
||||||
rbacRole, _ := rbac.RoleByName(roleName)
|
rbacRole, _ := rbac.RoleByName(roleName)
|
||||||
user.Roles = append(user.Roles, db2sdk.Role(rbacRole))
|
user.Roles = append(user.Roles, db2sdk.SlimRole(rbacRole))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||||
"github.com/coder/coder/v2/coderd/schedule"
|
"github.com/coder/coder/v2/coderd/schedule"
|
||||||
"github.com/coder/coder/v2/coderd/telemetry"
|
"github.com/coder/coder/v2/coderd/telemetry"
|
||||||
"github.com/coder/coder/v2/coderd/tracing"
|
"github.com/coder/coder/v2/coderd/tracing"
|
||||||
@ -631,6 +632,7 @@ func New(options *Options) *API {
|
|||||||
httpmw.AttachRequestID,
|
httpmw.AttachRequestID,
|
||||||
httpmw.ExtractRealIP(api.RealIPConfig),
|
httpmw.ExtractRealIP(api.RealIPConfig),
|
||||||
httpmw.Logger(api.Logger),
|
httpmw.Logger(api.Logger),
|
||||||
|
rolestore.CustomRoleMW,
|
||||||
prometheusMW,
|
prometheusMW,
|
||||||
// Build-Version is helpful for debugging.
|
// Build-Version is helpful for debugging.
|
||||||
func(next http.Handler) http.Handler {
|
func(next http.Handler) http.Handler {
|
||||||
@ -915,7 +917,7 @@ func New(options *Options) *API {
|
|||||||
r.Post("/logout", api.postLogout)
|
r.Post("/logout", api.postLogout)
|
||||||
// These routes query information about site wide roles.
|
// These routes query information about site wide roles.
|
||||||
r.Route("/roles", func(r chi.Router) {
|
r.Route("/roles", func(r chi.Router) {
|
||||||
r.Get("/", api.assignableSiteRoles)
|
r.Get("/", api.AssignableSiteRoles)
|
||||||
})
|
})
|
||||||
r.Route("/{user}", func(r chi.Router) {
|
r.Route("/{user}", func(r chi.Router) {
|
||||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/parameter"
|
"github.com/coder/coder/v2/coderd/parameter"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||||
@ -28,9 +29,25 @@ import (
|
|||||||
// database types to slices of codersdk types.
|
// database types to slices of codersdk types.
|
||||||
// Only works if the function takes a single argument.
|
// Only works if the function takes a single argument.
|
||||||
func List[F any, T any](list []F, convert func(F) T) []T {
|
func List[F any, T any](list []F, convert func(F) T) []T {
|
||||||
into := make([]T, 0, len(list))
|
return ListLazy(convert)(list)
|
||||||
for _, item := range list {
|
}
|
||||||
into = append(into, convert(item))
|
|
||||||
|
// ListLazy returns the converter function for a list, but does not eval
|
||||||
|
// the input. Helpful for combining the Map and the List functions.
|
||||||
|
func ListLazy[F any, T any](convert func(F) T) func(list []F) []T {
|
||||||
|
return func(list []F) []T {
|
||||||
|
into := make([]T, 0, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
into = append(into, convert(item))
|
||||||
|
}
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Map[K comparable, F any, T any](params map[K]F, convert func(F) T) map[K]T {
|
||||||
|
into := make(map[K]T)
|
||||||
|
for k, item := range params {
|
||||||
|
into[k] = convert(item)
|
||||||
}
|
}
|
||||||
return into
|
return into
|
||||||
}
|
}
|
||||||
@ -150,12 +167,20 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
|||||||
convertedUser := codersdk.User{
|
convertedUser := codersdk.User{
|
||||||
ReducedUser: ReducedUser(user),
|
ReducedUser: ReducedUser(user),
|
||||||
OrganizationIDs: organizationIDs,
|
OrganizationIDs: organizationIDs,
|
||||||
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
|
Roles: make([]codersdk.SlimRole, 0, len(user.RBACRoles)),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, roleName := range user.RBACRoles {
|
for _, roleName := range user.RBACRoles {
|
||||||
rbacRole, _ := rbac.RoleByName(roleName)
|
rbacRole, err := rbac.RoleByName(roleName)
|
||||||
convertedUser.Roles = append(convertedUser.Roles, Role(rbacRole))
|
if err == nil {
|
||||||
|
convertedUser.Roles = append(convertedUser.Roles, SlimRole(rbacRole))
|
||||||
|
} else {
|
||||||
|
// TODO: Fix this for custom roles to display the actual display_name
|
||||||
|
// Requires plumbing either a cached role value, or the db.
|
||||||
|
convertedUser.Roles = append(convertedUser.Roles, codersdk.SlimRole{
|
||||||
|
Name: roleName,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertedUser
|
return convertedUser
|
||||||
@ -180,8 +205,8 @@ func Group(group database.Group, members []database.User) codersdk.Group {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Role(role rbac.Role) codersdk.Role {
|
func SlimRole(role rbac.Role) codersdk.SlimRole {
|
||||||
return codersdk.Role{
|
return codersdk.SlimRole{
|
||||||
DisplayName: role.DisplayName,
|
DisplayName: role.DisplayName,
|
||||||
Name: role.Name,
|
Name: role.Name,
|
||||||
}
|
}
|
||||||
@ -500,3 +525,39 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Role(role rbac.Role) codersdk.Role {
|
||||||
|
return codersdk.Role{
|
||||||
|
Name: role.Name,
|
||||||
|
DisplayName: role.DisplayName,
|
||||||
|
SitePermissions: List(role.Site, Permission),
|
||||||
|
OrganizationPermissions: Map(role.Org, ListLazy(Permission)),
|
||||||
|
UserPermissions: List(role.Site, Permission),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Permission(permission rbac.Permission) codersdk.Permission {
|
||||||
|
return codersdk.Permission{
|
||||||
|
Negate: permission.Negate,
|
||||||
|
ResourceType: codersdk.RBACResource(permission.ResourceType),
|
||||||
|
Action: codersdk.RBACAction(permission.Action),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoleToRBAC(role codersdk.Role) rbac.Role {
|
||||||
|
return rbac.Role{
|
||||||
|
Name: role.Name,
|
||||||
|
DisplayName: role.DisplayName,
|
||||||
|
Site: List(role.SitePermissions, PermissionToRBAC),
|
||||||
|
Org: Map(role.OrganizationPermissions, ListLazy(PermissionToRBAC)),
|
||||||
|
User: List(role.UserPermissions, PermissionToRBAC),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PermissionToRBAC(permission codersdk.Permission) rbac.Permission {
|
||||||
|
return rbac.Permission{
|
||||||
|
Negate: permission.Negate,
|
||||||
|
ResourceType: string(permission.ResourceType),
|
||||||
|
Action: policy.Action(permission.Action),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -620,7 +620,8 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(customRoles) > 0 {
|
if len(customRoles) > 0 {
|
||||||
expandedCustomRoles, err := q.CustomRolesByName(ctx, customRoles)
|
// Leverage any custom role cache that might exist.
|
||||||
|
expandedCustomRoles, err := rolestore.Expand(ctx, q.db, customRoles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("fetching custom roles: %w", err)
|
return xerrors.Errorf("fetching custom roles: %w", err)
|
||||||
}
|
}
|
||||||
@ -632,7 +633,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
|
|||||||
// Stop at the first one found. We could make a better error that
|
// Stop at the first one found. We could make a better error that
|
||||||
// returns them all, but then someone could pass in a large list to make us do
|
// returns them all, but then someone could pass in a large list to make us do
|
||||||
// a lot of loop iterations.
|
// a lot of loop iterations.
|
||||||
if !slices.ContainsFunc(expandedCustomRoles, func(customRole database.CustomRole) bool {
|
if !slices.ContainsFunc(expandedCustomRoles, func(customRole rbac.Role) bool {
|
||||||
return strings.EqualFold(customRole.Name, role)
|
return strings.EqualFold(customRole.Name, role)
|
||||||
}) {
|
}) {
|
||||||
return xerrors.Errorf("%q is not a supported role", role)
|
return xerrors.Errorf("%q is not a supported role", role)
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
"github.com/coder/coder/v2/coderd/promoauth"
|
"github.com/coder/coder/v2/coderd/promoauth"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -437,11 +438,21 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocritic // Permission to lookup custom roles the user has assigned.
|
||||||
|
rbacRoles, err := rolestore.Expand(dbauthz.AsSystemRestricted(ctx), cfg.DB, roles.Roles)
|
||||||
|
if err != nil {
|
||||||
|
return write(http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to expand authenticated user roles",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Validations: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Actor is the user's authorization context.
|
// Actor is the user's authorization context.
|
||||||
actor := rbac.Subject{
|
actor := rbac.Subject{
|
||||||
FriendlyName: roles.Username,
|
FriendlyName: roles.Username,
|
||||||
ID: key.UserID.String(),
|
ID: key.UserID.String(),
|
||||||
Roles: rbac.RoleNames(roles.Roles),
|
Roles: rbacRoles,
|
||||||
Groups: roles.Groups,
|
Groups: roles.Groups,
|
||||||
Scope: rbac.ScopeName(key.Scope),
|
Scope: rbac.ScopeName(key.Scope),
|
||||||
}.WithCachedASTValue()
|
}.WithCachedASTValue()
|
||||||
|
@ -82,7 +82,7 @@ func (api *API) updateOrganizationMemberRoles(ctx context.Context, args database
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := rbac.RoleByName(r); err != nil {
|
if _, err := rbac.RoleByName(r); err != nil {
|
||||||
return database.OrganizationMember{}, xerrors.Errorf("%q is not a supported role", r)
|
return database.OrganizationMember{}, xerrors.Errorf("%q is not a supported organization role", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,12 +99,12 @@ func convertOrganizationMember(mem database.OrganizationMember) codersdk.Organiz
|
|||||||
OrganizationID: mem.OrganizationID,
|
OrganizationID: mem.OrganizationID,
|
||||||
CreatedAt: mem.CreatedAt,
|
CreatedAt: mem.CreatedAt,
|
||||||
UpdatedAt: mem.UpdatedAt,
|
UpdatedAt: mem.UpdatedAt,
|
||||||
Roles: make([]codersdk.Role, 0, len(mem.Roles)),
|
Roles: make([]codersdk.SlimRole, 0, len(mem.Roles)),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, roleName := range mem.Roles {
|
for _, roleName := range mem.Roles {
|
||||||
rbacRole, _ := rbac.RoleByName(roleName)
|
rbacRole, _ := rbac.RoleByName(roleName)
|
||||||
convertedMember.Roles = append(convertedMember.Roles, db2sdk.Role(rbacRole))
|
convertedMember.Roles = append(convertedMember.Roles, db2sdk.SlimRole(rbacRole))
|
||||||
}
|
}
|
||||||
return convertedMember
|
return convertedMember
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,96 @@
|
|||||||
package rolestore
|
package rolestore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/coderd/util/syncmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type customRoleCtxKey struct{}
|
||||||
|
|
||||||
|
// CustomRoleMW adds a custom role cache on the ctx to prevent duplicate
|
||||||
|
// db fetches.
|
||||||
|
func CustomRoleMW(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = r.WithContext(CustomRoleCacheContext(r.Context()))
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomRoleCacheContext prevents needing to lookup custom roles within the
|
||||||
|
// same request lifecycle. Optimizing this to span requests should be done
|
||||||
|
// in the future.
|
||||||
|
func CustomRoleCacheContext(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, customRoleCtxKey{}, syncmap.New[string, rbac.Role]())
|
||||||
|
}
|
||||||
|
|
||||||
|
func roleCache(ctx context.Context) *syncmap.Map[string, rbac.Role] {
|
||||||
|
c, ok := ctx.Value(customRoleCtxKey{}).(*syncmap.Map[string, rbac.Role])
|
||||||
|
if !ok {
|
||||||
|
return syncmap.New[string, rbac.Role]()
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand will expand built in roles, and fetch custom roles from the database.
|
||||||
|
func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles, error) {
|
||||||
|
if len(names) == 0 {
|
||||||
|
// That was easy
|
||||||
|
return []rbac.Role{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := roleCache(ctx)
|
||||||
|
lookup := make([]string, 0)
|
||||||
|
roles := make([]rbac.Role, 0, len(names))
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
// Remove any built in roles
|
||||||
|
expanded, err := rbac.RoleByName(name)
|
||||||
|
if err == nil {
|
||||||
|
roles = append(roles, expanded)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom role cache
|
||||||
|
customRole, ok := cache.Load(name)
|
||||||
|
if ok {
|
||||||
|
roles = append(roles, customRole)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer custom role lookup
|
||||||
|
lookup = append(lookup, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lookup) > 0 {
|
||||||
|
// If some roles are missing from the database, they are omitted from
|
||||||
|
// the expansion. These roles are no-ops. Should we raise some kind of
|
||||||
|
// warning when this happens?
|
||||||
|
dbroles, err := db.CustomRolesByName(ctx, lookup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("fetch custom roles: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert dbroles -> roles
|
||||||
|
for _, dbrole := range dbroles {
|
||||||
|
converted, err := ConvertDBRole(dbrole)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("convert db role %q: %w", dbrole, err)
|
||||||
|
}
|
||||||
|
roles = append(roles, converted)
|
||||||
|
cache.Store(dbrole.Name, converted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
||||||
role := rbac.Role{
|
role := rbac.Role{
|
||||||
Name: dbRole.Name,
|
Name: dbRole.Name,
|
||||||
@ -35,3 +117,30 @@ func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
|||||||
|
|
||||||
return role, nil
|
return role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConvertRoleToDB(role rbac.Role) (database.CustomRole, error) {
|
||||||
|
dbRole := database.CustomRole{
|
||||||
|
Name: role.Name,
|
||||||
|
DisplayName: role.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
siteData, err := json.Marshal(role.Site)
|
||||||
|
if err != nil {
|
||||||
|
return dbRole, xerrors.Errorf("marshal site permissions: %w", err)
|
||||||
|
}
|
||||||
|
dbRole.SitePermissions = siteData
|
||||||
|
|
||||||
|
orgData, err := json.Marshal(role.Org)
|
||||||
|
if err != nil {
|
||||||
|
return dbRole, xerrors.Errorf("marshal org permissions: %w", err)
|
||||||
|
}
|
||||||
|
dbRole.OrgPermissions = orgData
|
||||||
|
|
||||||
|
userData, err := json.Marshal(role.User)
|
||||||
|
if err != nil {
|
||||||
|
return dbRole, xerrors.Errorf("marshal user permissions: %w", err)
|
||||||
|
}
|
||||||
|
dbRole.UserPermissions = userData
|
||||||
|
|
||||||
|
return dbRole, nil
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
)
|
)
|
||||||
|
|
||||||
// assignableSiteRoles returns all site wide roles that can be assigned.
|
// AssignableSiteRoles returns all site wide roles that can be assigned.
|
||||||
//
|
//
|
||||||
// @Summary Get site member roles
|
// @Summary Get site member roles
|
||||||
// @ID get-site-member-roles
|
// @ID get-site-member-roles
|
||||||
@ -20,7 +20,7 @@ import (
|
|||||||
// @Tags Members
|
// @Tags Members
|
||||||
// @Success 200 {array} codersdk.AssignableRoles
|
// @Success 200 {array} codersdk.AssignableRoles
|
||||||
// @Router /users/roles [get]
|
// @Router /users/roles [get]
|
||||||
func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) AssignableSiteRoles(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
actorRoles := httpmw.UserAuthorization(r)
|
actorRoles := httpmw.UserAuthorization(r)
|
||||||
if !api.Authorize(r, policy.ActionRead, rbac.ResourceAssignRole) {
|
if !api.Authorize(r, policy.ActionRead, rbac.ResourceAssignRole) {
|
||||||
@ -32,7 +32,7 @@ func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles))
|
httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles))
|
||||||
}
|
}
|
||||||
|
|
||||||
// assignableSiteRoles returns all org wide roles that can be assigned.
|
// assignableOrgRoles returns all org wide roles that can be assigned.
|
||||||
//
|
//
|
||||||
// @Summary Get member roles by organization
|
// @Summary Get member roles by organization
|
||||||
// @ID get-member-roles-by-organization
|
// @ID get-member-roles-by-organization
|
||||||
@ -66,7 +66,7 @@ func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role) []coder
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
assignable = append(assignable, codersdk.AssignableRoles{
|
assignable = append(assignable, codersdk.AssignableRoles{
|
||||||
Role: codersdk.Role{
|
SlimRole: codersdk.SlimRole{
|
||||||
Name: role.Name,
|
Name: role.Name,
|
||||||
DisplayName: role.DisplayName,
|
DisplayName: role.DisplayName,
|
||||||
},
|
},
|
||||||
|
@ -143,9 +143,9 @@ func TestListRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertRole(roleName string) codersdk.Role {
|
func convertRole(roleName string) codersdk.SlimRole {
|
||||||
role, _ := rbac.RoleByName(roleName)
|
role, _ := rbac.RoleByName(roleName)
|
||||||
return codersdk.Role{
|
return codersdk.SlimRole{
|
||||||
DisplayName: role.DisplayName,
|
DisplayName: role.DisplayName,
|
||||||
Name: role.Name,
|
Name: role.Name,
|
||||||
}
|
}
|
||||||
@ -156,7 +156,7 @@ func convertRoles(assignableRoles map[string]bool) []codersdk.AssignableRoles {
|
|||||||
for roleName, assignable := range assignableRoles {
|
for roleName, assignable := range assignableRoles {
|
||||||
role := convertRole(roleName)
|
role := convertRole(roleName)
|
||||||
converted = append(converted, codersdk.AssignableRoles{
|
converted = append(converted, codersdk.AssignableRoles{
|
||||||
Role: role,
|
SlimRole: role,
|
||||||
Assignable: assignable,
|
Assignable: assignable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1095,7 +1095,7 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedUser, err := UpdateSiteUserRoles(ctx, api.Database, database.UpdateUserRolesParams{
|
updatedUser, err := api.Database.UpdateUserRoles(ctx, database.UpdateUserRolesParams{
|
||||||
GrantedRoles: params.Roles,
|
GrantedRoles: params.Roles,
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
})
|
})
|
||||||
@ -1123,27 +1123,6 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizationIDs))
|
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizationIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSiteUserRoles will ensure only site wide roles are passed in as arguments.
|
|
||||||
// If an organization role is included, an error is returned.
|
|
||||||
func UpdateSiteUserRoles(ctx context.Context, db database.Store, args database.UpdateUserRolesParams) (database.User, error) {
|
|
||||||
// Enforce only site wide roles.
|
|
||||||
for _, r := range args.GrantedRoles {
|
|
||||||
if _, ok := rbac.IsOrgRole(r); ok {
|
|
||||||
return database.User{}, xerrors.Errorf("Must only update site wide roles")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := rbac.RoleByName(r); err != nil {
|
|
||||||
return database.User{}, xerrors.Errorf("%q is not a supported role", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedUser, err := db.UpdateUserRoles(ctx, args)
|
|
||||||
if err != nil {
|
|
||||||
return database.User{}, xerrors.Errorf("update site roles: %w", err)
|
|
||||||
}
|
|
||||||
return updatedUser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns organizations the parameterized user has access to.
|
// Returns organizations the parameterized user has access to.
|
||||||
//
|
//
|
||||||
// @Summary Get organizations by user
|
// @Summary Get organizations by user
|
||||||
|
@ -1049,7 +1049,7 @@ func TestGrantSiteRoles(t *testing.T) {
|
|||||||
c.AssignToUser = newUser.ID.String()
|
c.AssignToUser = newUser.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var newRoles []codersdk.Role
|
var newRoles []codersdk.SlimRole
|
||||||
if c.OrgID != uuid.Nil {
|
if c.OrgID != uuid.Nil {
|
||||||
// Org assign
|
// Org assign
|
||||||
var mem codersdk.OrganizationMember
|
var mem codersdk.OrganizationMember
|
||||||
|
@ -54,6 +54,7 @@ const (
|
|||||||
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
||||||
FeatureAccessControl FeatureName = "access_control"
|
FeatureAccessControl FeatureName = "access_control"
|
||||||
FeatureControlSharedPorts FeatureName = "control_shared_ports"
|
FeatureControlSharedPorts FeatureName = "control_shared_ports"
|
||||||
|
FeatureCustomRoles FeatureName = "custom_roles"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FeatureNames must be kept in-sync with the Feature enum above.
|
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||||
@ -74,6 +75,7 @@ var FeatureNames = []FeatureName{
|
|||||||
FeatureWorkspaceBatchActions,
|
FeatureWorkspaceBatchActions,
|
||||||
FeatureAccessControl,
|
FeatureAccessControl,
|
||||||
FeatureControlSharedPorts,
|
FeatureControlSharedPorts,
|
||||||
|
FeatureCustomRoles,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Humanize returns the feature name in a human-readable format.
|
// Humanize returns the feature name in a human-readable format.
|
||||||
@ -98,6 +100,7 @@ func (n FeatureName) AlwaysEnable() bool {
|
|||||||
FeatureAppearance: true,
|
FeatureAppearance: true,
|
||||||
FeatureWorkspaceBatchActions: true,
|
FeatureWorkspaceBatchActions: true,
|
||||||
FeatureHighAvailability: true,
|
FeatureHighAvailability: true,
|
||||||
|
FeatureCustomRoles: true,
|
||||||
}[n]
|
}[n]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2218,6 +2221,7 @@ const (
|
|||||||
ExperimentExample Experiment = "example" // This isn't used for anything.
|
ExperimentExample Experiment = "example" // This isn't used for anything.
|
||||||
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
|
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
|
||||||
ExperimentMultiOrganization Experiment = "multi-organization" // Requires organization context for interactions, default org is assumed.
|
ExperimentMultiOrganization Experiment = "multi-organization" // Requires organization context for interactions, default org is assumed.
|
||||||
|
ExperimentCustomRoles Experiment = "custom-roles" // Allows creating runtime custom roles
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExperimentsAll should include all experiments that are safe for
|
// ExperimentsAll should include all experiments that are safe for
|
||||||
|
@ -48,11 +48,11 @@ type Organization struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrganizationMember struct {
|
type OrganizationMember struct {
|
||||||
UserID uuid.UUID `db:"user_id" json:"user_id" format:"uuid"`
|
UserID uuid.UUID `db:"user_id" json:"user_id" format:"uuid"`
|
||||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id" format:"uuid"`
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id" format:"uuid"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at" format:"date-time"`
|
CreatedAt time.Time `db:"created_at" json:"created_at" format:"date-time"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at" format:"date-time"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at" format:"date-time"`
|
||||||
Roles []Role `db:"roles" json:"roles"`
|
Roles []SlimRole `db:"roles" json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTemplateVersionRequest enables callers to create a new Template Version.
|
// CreateTemplateVersionRequest enables callers to create a new Template Version.
|
||||||
|
@ -9,16 +9,52 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Role struct {
|
// SlimRole omits permission information from a role.
|
||||||
|
// At present, this is because our apis do not return permission information,
|
||||||
|
// and it would require extra db calls to fetch this information. The UI does
|
||||||
|
// not need it, so most api calls will use this structure that omits information.
|
||||||
|
type SlimRole struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssignableRoles struct {
|
type AssignableRoles struct {
|
||||||
Role
|
SlimRole
|
||||||
Assignable bool `json:"assignable"`
|
Assignable bool `json:"assignable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permission is the format passed into the rego.
|
||||||
|
type Permission struct {
|
||||||
|
// Negate makes this a negative permission
|
||||||
|
Negate bool `json:"negate"`
|
||||||
|
ResourceType RBACResource `json:"resource_type"`
|
||||||
|
Action RBACAction `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role is a longer form of SlimRole used to edit custom roles.
|
||||||
|
type Role struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
SitePermissions []Permission `json:"site_permissions"`
|
||||||
|
// map[<org_id>] -> Permissions
|
||||||
|
OrganizationPermissions map[string][]Permission `json:"organization_permissions"`
|
||||||
|
UserPermissions []Permission `json:"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)
|
||||||
|
if err != nil {
|
||||||
|
return Role{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return Role{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
return role, json.NewDecoder(res.Body).Decode(&role)
|
||||||
|
}
|
||||||
|
|
||||||
// ListSiteRoles lists all assignable site wide roles.
|
// ListSiteRoles lists all assignable site wide roles.
|
||||||
func (c *Client) ListSiteRoles(ctx context.Context) ([]AssignableRoles, error) {
|
func (c *Client) ListSiteRoles(ctx context.Context) ([]AssignableRoles, error) {
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/roles", nil)
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/roles", nil)
|
||||||
@ -46,3 +82,17 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]As
|
|||||||
var roles []AssignableRoles
|
var roles []AssignableRoles
|
||||||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreatePermissions is a helper function to quickly build permissions.
|
||||||
|
func CreatePermissions(mapping map[RBACResource][]RBACAction) []Permission {
|
||||||
|
perms := make([]Permission, 0)
|
||||||
|
for t, actions := range mapping {
|
||||||
|
for _, action := range actions {
|
||||||
|
perms = append(perms, Permission{
|
||||||
|
ResourceType: t,
|
||||||
|
Action: action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return perms
|
||||||
|
}
|
||||||
|
@ -63,7 +63,7 @@ type User struct {
|
|||||||
ReducedUser `table:"r,recursive_inline"`
|
ReducedUser `table:"r,recursive_inline"`
|
||||||
|
|
||||||
OrganizationIDs []uuid.UUID `json:"organization_ids" format:"uuid"`
|
OrganizationIDs []uuid.UUID `json:"organization_ids" format:"uuid"`
|
||||||
Roles []Role `json:"roles"`
|
Roles []SlimRole `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUsersResponse struct {
|
type GetUsersResponse struct {
|
||||||
|
124
docs/api/members.md
generated
124
docs/api/members.md
generated
@ -154,3 +154,127 @@ Status Code **200**
|
|||||||
| `» name` | string | false | | |
|
| `» name` | string | false | | |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
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_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_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).
|
||||||
|
151
docs/api/schemas.md
generated
151
docs/api/schemas.md
generated
@ -2694,6 +2694,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| `example` |
|
| `example` |
|
||||||
| `auto-fill-parameters` |
|
| `auto-fill-parameters` |
|
||||||
| `multi-organization` |
|
| `multi-organization` |
|
||||||
|
| `custom-roles` |
|
||||||
|
|
||||||
## codersdk.ExternalAuth
|
## codersdk.ExternalAuth
|
||||||
|
|
||||||
@ -3579,13 +3580,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Required | Restrictions | Description |
|
| Name | Type | Required | Restrictions | Description |
|
||||||
| ----------------- | --------------------------------------- | -------- | ------------ | ----------- |
|
| ----------------- | ----------------------------------------------- | -------- | ------------ | ----------- |
|
||||||
| `created_at` | string | false | | |
|
| `created_at` | string | false | | |
|
||||||
| `organization_id` | string | false | | |
|
| `organization_id` | string | false | | |
|
||||||
| `roles` | array of [codersdk.Role](#codersdkrole) | false | | |
|
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||||
| `updated_at` | string | false | | |
|
| `updated_at` | string | false | | |
|
||||||
| `user_id` | string | false | | |
|
| `user_id` | string | false | | |
|
||||||
|
|
||||||
## codersdk.PatchGroupRequest
|
## codersdk.PatchGroupRequest
|
||||||
|
|
||||||
@ -3649,6 +3650,24 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| `name` | string | true | | |
|
| `name` | string | true | | |
|
||||||
| `regenerate_token` | boolean | false | | |
|
| `regenerate_token` | boolean | false | | |
|
||||||
|
|
||||||
|
## codersdk.Permission
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "application_connect",
|
||||||
|
"negate": true,
|
||||||
|
"resource_type": "*"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| --------------- | ---------------------------------------------- | -------- | ------------ | --------------------------------------- |
|
||||||
|
| `action` | [codersdk.RBACAction](#codersdkrbacaction) | false | | |
|
||||||
|
| `negate` | boolean | false | | Negate makes this a negative permission |
|
||||||
|
| `resource_type` | [codersdk.RBACResource](#codersdkrbacresource) | false | | |
|
||||||
|
|
||||||
## codersdk.PostOAuth2ProviderAppRequest
|
## codersdk.PostOAuth2ProviderAppRequest
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -4271,16 +4290,50 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"display_name": "string",
|
"display_name": "string",
|
||||||
"name": "string"
|
"name": "string",
|
||||||
|
"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": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Required | Restrictions | Description |
|
| Name | Type | Required | Restrictions | Description |
|
||||||
| -------------- | ------ | -------- | ------------ | ----------- |
|
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- |
|
||||||
| `display_name` | string | false | | |
|
| `display_name` | string | false | | |
|
||||||
| `name` | string | false | | |
|
| `name` | 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 | | |
|
||||||
|
|
||||||
## codersdk.SSHConfig
|
## codersdk.SSHConfig
|
||||||
|
|
||||||
@ -4356,6 +4409,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| `disable_expiry_refresh` | boolean | false | | Disable expiry refresh will disable automatically refreshing api keys when they are used from the api. This means the api key lifetime at creation is the lifetime of the api key. |
|
| `disable_expiry_refresh` | boolean | false | | Disable expiry refresh will disable automatically refreshing api keys when they are used from the api. This means the api key lifetime at creation is the lifetime of the api key. |
|
||||||
| `max_token_lifetime` | integer | false | | |
|
| `max_token_lifetime` | integer | false | | |
|
||||||
|
|
||||||
|
## codersdk.SlimRole
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"display_name": "string",
|
||||||
|
"name": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| -------------- | ------ | -------- | ------------ | ----------- |
|
||||||
|
| `display_name` | string | false | | |
|
||||||
|
| `name` | string | false | | |
|
||||||
|
|
||||||
## codersdk.SupportConfig
|
## codersdk.SupportConfig
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -4906,21 +4975,21 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Required | Restrictions | Description |
|
| Name | Type | Required | Restrictions | Description |
|
||||||
| ------------------ | ---------------------------------------------- | -------- | ------------ | ----------- |
|
| ------------------ | ----------------------------------------------- | -------- | ------------ | ----------- |
|
||||||
| `avatar_url` | string | false | | |
|
| `avatar_url` | string | false | | |
|
||||||
| `created_at` | string | true | | |
|
| `created_at` | string | true | | |
|
||||||
| `email` | string | true | | |
|
| `email` | string | true | | |
|
||||||
| `id` | string | true | | |
|
| `id` | string | true | | |
|
||||||
| `last_seen_at` | string | false | | |
|
| `last_seen_at` | string | false | | |
|
||||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||||
| `name` | string | false | | |
|
| `name` | string | false | | |
|
||||||
| `organization_ids` | array of string | false | | |
|
| `organization_ids` | array of string | false | | |
|
||||||
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
|
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
|
||||||
| `roles` | array of [codersdk.Role](#codersdkrole) | false | | |
|
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||||
| `theme_preference` | string | false | | |
|
| `theme_preference` | string | false | | |
|
||||||
| `username` | string | true | | |
|
| `username` | string | true | | |
|
||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
@ -5497,20 +5566,20 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Required | Restrictions | Description |
|
| Name | Type | Required | Restrictions | Description |
|
||||||
| ------------------ | ------------------------------------------ | -------- | ------------ | ----------- |
|
| ------------------ | ----------------------------------------------- | -------- | ------------ | ----------- |
|
||||||
| `avatar_url` | string | false | | |
|
| `avatar_url` | string | false | | |
|
||||||
| `created_at` | string | true | | |
|
| `created_at` | string | true | | |
|
||||||
| `email` | string | true | | |
|
| `email` | string | true | | |
|
||||||
| `id` | string | true | | |
|
| `id` | string | true | | |
|
||||||
| `last_seen_at` | string | false | | |
|
| `last_seen_at` | string | false | | |
|
||||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||||
| `name` | string | false | | |
|
| `name` | string | false | | |
|
||||||
| `organization_ids` | array of string | false | | |
|
| `organization_ids` | array of string | false | | |
|
||||||
| `roles` | array of [codersdk.Role](#codersdkrole) | false | | |
|
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||||
| `theme_preference` | string | false | | |
|
| `theme_preference` | string | false | | |
|
||||||
| `username` | string | true | | |
|
| `username` | string | true | | |
|
||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
|
@ -326,6 +326,23 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||||||
r.Put("/", api.putAppearance)
|
r.Put("/", api.putAppearance)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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.Route("/users/{user}/quiet-hours", func(r chi.Router) {
|
||||||
r.Use(
|
r.Use(
|
||||||
api.autostopRequirementEnabledMW,
|
api.autostopRequirementEnabledMW,
|
||||||
|
80
enterprise/coderd/roles.go
Normal file
80
enterprise/coderd/roles.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package coderd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||||
|
"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()
|
||||||
|
|
||||||
|
var req codersdk.Role
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.OrganizationPermissions) > 0 {
|
||||||
|
// Org perms should be assigned only in org specific roles. Otherwise,
|
||||||
|
// it gets complicated to keep track of who can do what.
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Invalid request, not allowed to assign organization permissions for a site wide role.",
|
||||||
|
Detail: "site wide roles may not contain organization specific permissions",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure all permissions inputted are valid according to our policy.
|
||||||
|
rbacRole := db2sdk.RoleToRBAC(req)
|
||||||
|
args, err := rolestore.ConvertRoleToDB(rbacRole)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Invalid request",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||||
|
Name: args.Name,
|
||||||
|
DisplayName: args.DisplayName,
|
||||||
|
SitePermissions: args.SitePermissions,
|
||||||
|
OrgPermissions: args.OrgPermissions,
|
||||||
|
UserPermissions: args.UserPermissions,
|
||||||
|
})
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Failed to update role permissions",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedInsert, err := rolestore.ConvertDBRole(inserted)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Permissions were updated, unable to read them back out of the database.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(convertedInsert))
|
||||||
|
}
|
170
enterprise/coderd/roles_test.go
Normal file
170
enterprise/coderd/roles_test.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package coderd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/coder/coder/v2/provisioner/echo"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCustomRole(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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create, assign, and use a custom role
|
||||||
|
t.Run("Success", 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
|
||||||
|
role, err := owner.PatchRole(ctx, templateAdminCustom)
|
||||||
|
require.NoError(t, err, "upsert role")
|
||||||
|
|
||||||
|
// Assign the custom template admin role
|
||||||
|
tmplAdmin, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name)
|
||||||
|
|
||||||
|
// Assert the role exists
|
||||||
|
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
|
||||||
|
// TODO: Turn this assertion back on when the cli api experience is created.
|
||||||
|
//allRoles, err := tmplAdmin.ListSiteRoles(ctx)
|
||||||
|
//require.NoError(t, err)
|
||||||
|
//
|
||||||
|
//require.True(t, slices.ContainsFunc(allRoles, func(selected codersdk.AssignableRoles) bool {
|
||||||
|
// return selected.Name == role.Name
|
||||||
|
//}), "role missing from site role list")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Revoked licenses cannot modify/create custom roles, but they can
|
||||||
|
// use the existing roles.
|
||||||
|
t.Run("Revoked License", 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
|
||||||
|
role, err := owner.PatchRole(ctx, templateAdminCustom)
|
||||||
|
require.NoError(t, err, "upsert role")
|
||||||
|
|
||||||
|
// Remove the license to block enterprise functionality
|
||||||
|
licenses, err := owner.Licenses(ctx)
|
||||||
|
require.NoError(t, err, "get licenses")
|
||||||
|
for _, license := range licenses {
|
||||||
|
// Should be only 1...
|
||||||
|
err := owner.DeleteLicense(ctx, license.ID)
|
||||||
|
require.NoError(t, err, "delete license")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify functionality is lost
|
||||||
|
_, err = owner.PatchRole(ctx, templateAdminCustom)
|
||||||
|
require.ErrorContains(t, err, "Custom roles is an Enterprise feature", "upsert role")
|
||||||
|
|
||||||
|
// Assign the custom template admin role
|
||||||
|
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name)
|
||||||
|
|
||||||
|
// Try to create a template version, eg using the custom role
|
||||||
|
coderdtest.CreateTemplateVersion(t, tmplAdmin, first.OrganizationID, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Role patches are complete, as in the request overrides the existing role.
|
||||||
|
t.Run("RoleOverrides", 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
|
||||||
|
role, err := owner.PatchRole(ctx, templateAdminCustom)
|
||||||
|
require.NoError(t, err, "upsert role")
|
||||||
|
|
||||||
|
// Assign the custom template admin role
|
||||||
|
tmplAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, role.Name)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "upsert role with override")
|
||||||
|
|
||||||
|
// The role should no longer have template perms
|
||||||
|
data, err := echo.TarWithOptions(ctx, tmplAdmin.Logger(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
file, err := tmplAdmin.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader(data))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = tmplAdmin.CreateTemplateVersion(ctx, first.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||||
|
FileID: file.ID,
|
||||||
|
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
||||||
|
Provisioner: codersdk.ProvisionerTypeEcho,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "forbidden")
|
||||||
|
})
|
||||||
|
}
|
@ -7,7 +7,6 @@ import (
|
|||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"github.com/coder/coder/v2/coderd"
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
@ -96,7 +95,7 @@ func (api *API) setUserSiteRoles(ctx context.Context, logger slog.Logger, db dat
|
|||||||
|
|
||||||
// Should this be feature protected?
|
// Should this be feature protected?
|
||||||
return db.InTx(func(tx database.Store) error {
|
return db.InTx(func(tx database.Store) error {
|
||||||
_, err := coderd.UpdateSiteUserRoles(ctx, db, database.UpdateUserRolesParams{
|
_, err := db.UpdateUserRoles(ctx, database.UpdateUserRolesParams{
|
||||||
GrantedRoles: roles,
|
GrantedRoles: roles,
|
||||||
ID: userID,
|
ID: userID,
|
||||||
})
|
})
|
||||||
|
@ -14,6 +14,31 @@ import (
|
|||||||
"github.com/coder/coder/v2/codersdk"
|
"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 {
|
func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
// Entitlement must be enabled.
|
// Entitlement must be enabled.
|
||||||
|
@ -1102,7 +1102,7 @@ class ApiMethods {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateUserRoles = async (
|
updateUserRoles = async (
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.SlimRole["name"][],
|
||||||
userId: TypesGen.User["id"],
|
userId: TypesGen.User["id"],
|
||||||
): Promise<TypesGen.User> => {
|
): Promise<TypesGen.User> => {
|
||||||
const response = await this.axios.put<TypesGen.User>(
|
const response = await this.axios.put<TypesGen.User>(
|
||||||
|
26
site/src/api/typesGenerated.ts
generated
26
site/src/api/typesGenerated.ts
generated
@ -65,7 +65,7 @@ export interface ArchiveTemplateVersionsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/roles.go
|
// From codersdk/roles.go
|
||||||
export interface AssignableRoles extends Role {
|
export interface AssignableRoles extends SlimRole {
|
||||||
readonly assignable: boolean;
|
readonly assignable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -786,7 +786,7 @@ export interface OrganizationMember {
|
|||||||
readonly organization_id: string;
|
readonly organization_id: string;
|
||||||
readonly created_at: string;
|
readonly created_at: string;
|
||||||
readonly updated_at: string;
|
readonly updated_at: string;
|
||||||
readonly roles: readonly Role[];
|
readonly roles: readonly SlimRole[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/pagination.go
|
// From codersdk/pagination.go
|
||||||
@ -821,6 +821,13 @@ export interface PatchWorkspaceProxy {
|
|||||||
readonly regenerate_token: boolean;
|
readonly regenerate_token: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/roles.go
|
||||||
|
export interface Permission {
|
||||||
|
readonly negate: boolean;
|
||||||
|
readonly resource_type: RBACResource;
|
||||||
|
readonly action: RBACAction;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/oauth2.go
|
// From codersdk/oauth2.go
|
||||||
export interface PostOAuth2ProviderAppRequest {
|
export interface PostOAuth2ProviderAppRequest {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@ -970,6 +977,9 @@ export interface Response {
|
|||||||
export interface Role {
|
export interface Role {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly display_name: string;
|
readonly display_name: string;
|
||||||
|
readonly site_permissions: readonly Permission[];
|
||||||
|
readonly organization_permissions: Record<string, readonly Permission[]>;
|
||||||
|
readonly user_permissions: readonly Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
@ -1014,6 +1024,12 @@ export interface SessionLifetime {
|
|||||||
readonly max_token_lifetime?: number;
|
readonly max_token_lifetime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/roles.go
|
||||||
|
export interface SlimRole {
|
||||||
|
readonly name: string;
|
||||||
|
readonly display_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export interface SupportConfig {
|
export interface SupportConfig {
|
||||||
readonly links: readonly LinkConfig[];
|
readonly links: readonly LinkConfig[];
|
||||||
@ -1405,7 +1421,7 @@ export interface UpsertWorkspaceAgentPortShareRequest {
|
|||||||
// From codersdk/users.go
|
// From codersdk/users.go
|
||||||
export interface User extends ReducedUser {
|
export interface User extends ReducedUser {
|
||||||
readonly organization_ids: readonly string[];
|
readonly organization_ids: readonly string[];
|
||||||
readonly roles: readonly Role[];
|
readonly roles: readonly SlimRole[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/insights.go
|
// From codersdk/insights.go
|
||||||
@ -1910,10 +1926,12 @@ export const Entitlements: Entitlement[] = [
|
|||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export type Experiment =
|
export type Experiment =
|
||||||
| "auto-fill-parameters"
|
| "auto-fill-parameters"
|
||||||
|
| "custom-roles"
|
||||||
| "example"
|
| "example"
|
||||||
| "multi-organization";
|
| "multi-organization";
|
||||||
export const Experiments: Experiment[] = [
|
export const Experiments: Experiment[] = [
|
||||||
"auto-fill-parameters",
|
"auto-fill-parameters",
|
||||||
|
"custom-roles",
|
||||||
"example",
|
"example",
|
||||||
"multi-organization",
|
"multi-organization",
|
||||||
];
|
];
|
||||||
@ -1926,6 +1944,7 @@ export type FeatureName =
|
|||||||
| "audit_log"
|
| "audit_log"
|
||||||
| "browser_only"
|
| "browser_only"
|
||||||
| "control_shared_ports"
|
| "control_shared_ports"
|
||||||
|
| "custom_roles"
|
||||||
| "external_provisioner_daemons"
|
| "external_provisioner_daemons"
|
||||||
| "external_token_encryption"
|
| "external_token_encryption"
|
||||||
| "high_availability"
|
| "high_availability"
|
||||||
@ -1943,6 +1962,7 @@ export const FeatureNames: FeatureName[] = [
|
|||||||
"audit_log",
|
"audit_log",
|
||||||
"browser_only",
|
"browser_only",
|
||||||
"control_shared_ports",
|
"control_shared_ports",
|
||||||
|
"custom_roles",
|
||||||
"external_provisioner_daemons",
|
"external_provisioner_daemons",
|
||||||
"external_token_encryption",
|
"external_token_encryption",
|
||||||
"high_availability",
|
"high_availability",
|
||||||
|
@ -2,7 +2,7 @@ import { fireEvent, screen, within } from "@testing-library/react";
|
|||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { HttpResponse, http } from "msw";
|
import { HttpResponse, http } from "msw";
|
||||||
import { API } from "api/api";
|
import { API } from "api/api";
|
||||||
import type { Role } from "api/typesGenerated";
|
import type { SlimRole } from "api/typesGenerated";
|
||||||
import {
|
import {
|
||||||
MockUser,
|
MockUser,
|
||||||
MockUser2,
|
MockUser2,
|
||||||
@ -102,7 +102,7 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
|
|||||||
fireEvent.click(confirmButton);
|
fireEvent.click(confirmButton);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUserRole = async (role: Role) => {
|
const updateUserRole = async (role: SlimRole) => {
|
||||||
// Get the first user in the table
|
// Get the first user in the table
|
||||||
const users = await screen.findAllByText(/.*@coder.com/);
|
const users = await screen.findAllByText(/.*@coder.com/);
|
||||||
const userRow = users[0].closest("tr");
|
const userRow = users[0].closest("tr");
|
||||||
|
@ -25,7 +25,7 @@ export interface UsersPageViewProps {
|
|||||||
onResetUserPassword: (user: TypesGen.User) => void;
|
onResetUserPassword: (user: TypesGen.User) => void;
|
||||||
onUpdateUserRoles: (
|
onUpdateUserRoles: (
|
||||||
user: TypesGen.User,
|
user: TypesGen.User,
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.SlimRole["name"][],
|
||||||
) => void;
|
) => void;
|
||||||
filterProps: ComponentProps<typeof UsersFilter>;
|
filterProps: ComponentProps<typeof UsersFilter>;
|
||||||
isNonInitialPage: boolean;
|
isNonInitialPage: boolean;
|
||||||
|
@ -3,7 +3,7 @@ import UserIcon from "@mui/icons-material/PersonOutline";
|
|||||||
import Checkbox from "@mui/material/Checkbox";
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import type { Role } from "api/typesGenerated";
|
import type { SlimRole } from "api/typesGenerated";
|
||||||
import {
|
import {
|
||||||
HelpTooltip,
|
HelpTooltip,
|
||||||
HelpTooltipContent,
|
HelpTooltipContent,
|
||||||
@ -69,9 +69,9 @@ const Option: FC<OptionProps> = ({
|
|||||||
|
|
||||||
export interface EditRolesButtonProps {
|
export interface EditRolesButtonProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
roles: readonly Role[];
|
roles: readonly SlimRole[];
|
||||||
selectedRoleNames: Set<string>;
|
selectedRoleNames: Set<string>;
|
||||||
onChange: (roles: Role["name"][]) => void;
|
onChange: (roles: SlimRole["name"][]) => void;
|
||||||
isDefaultOpen?: boolean;
|
isDefaultOpen?: boolean;
|
||||||
oidcRoleSync: boolean;
|
oidcRoleSync: boolean;
|
||||||
userLoginType: string;
|
userLoginType: string;
|
||||||
|
@ -17,7 +17,7 @@ import { useTheme } from "@emotion/react";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import TableCell from "@mui/material/TableCell";
|
import TableCell from "@mui/material/TableCell";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import type { Role, User } from "api/typesGenerated";
|
import type { SlimRole, User } from "api/typesGenerated";
|
||||||
import { Pill } from "components/Pill/Pill";
|
import { Pill } from "components/Pill/Pill";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@ -28,7 +28,7 @@ import { EditRolesButton } from "./EditRolesButton";
|
|||||||
|
|
||||||
type UserRoleCellProps = {
|
type UserRoleCellProps = {
|
||||||
canEditUsers: boolean;
|
canEditUsers: boolean;
|
||||||
allAvailableRoles: Role[] | undefined;
|
allAvailableRoles: SlimRole[] | undefined;
|
||||||
user: User;
|
user: User;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
oidcRoleSyncEnabled: boolean;
|
oidcRoleSyncEnabled: boolean;
|
||||||
@ -90,7 +90,7 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type OverflowRolePillProps = {
|
type OverflowRolePillProps = {
|
||||||
roles: readonly Role[];
|
roles: readonly SlimRole[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||||
@ -148,7 +148,7 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fallbackRole: Role = {
|
const fallbackRole: SlimRole = {
|
||||||
name: "member",
|
name: "member",
|
||||||
display_name: "Member",
|
display_name: "Member",
|
||||||
} as const;
|
} as const;
|
||||||
@ -160,7 +160,9 @@ const roleNamesByAccessLevel: readonly string[] = [
|
|||||||
"auditor",
|
"auditor",
|
||||||
];
|
];
|
||||||
|
|
||||||
function sortRolesByAccessLevel(roles: readonly Role[]): readonly Role[] {
|
function sortRolesByAccessLevel(
|
||||||
|
roles: readonly SlimRole[],
|
||||||
|
): readonly SlimRole[] {
|
||||||
if (roles.length === 0) {
|
if (roles.length === 0) {
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
@ -172,7 +174,7 @@ function sortRolesByAccessLevel(roles: readonly Role[]): readonly Role[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedRoleNames(roles: readonly Role[]) {
|
function getSelectedRoleNames(roles: readonly SlimRole[]) {
|
||||||
const roleNameSet = new Set(roles.map((role) => role.name));
|
const roleNameSet = new Set(roles.map((role) => role.name));
|
||||||
if (roleNameSet.size === 0) {
|
if (roleNameSet.size === 0) {
|
||||||
roleNameSet.add(fallbackRole.name);
|
roleNameSet.add(fallbackRole.name);
|
||||||
|
@ -36,7 +36,7 @@ export interface UsersTableProps {
|
|||||||
onResetUserPassword: (user: TypesGen.User) => void;
|
onResetUserPassword: (user: TypesGen.User) => void;
|
||||||
onUpdateUserRoles: (
|
onUpdateUserRoles: (
|
||||||
user: TypesGen.User,
|
user: TypesGen.User,
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.SlimRole["name"][],
|
||||||
) => void;
|
) => void;
|
||||||
isNonInitialPage: boolean;
|
isNonInitialPage: boolean;
|
||||||
actorID: string;
|
actorID: string;
|
||||||
|
@ -52,7 +52,7 @@ interface UsersTableBodyProps {
|
|||||||
onResetUserPassword: (user: TypesGen.User) => void;
|
onResetUserPassword: (user: TypesGen.User) => void;
|
||||||
onUpdateUserRoles: (
|
onUpdateUserRoles: (
|
||||||
user: TypesGen.User,
|
user: TypesGen.User,
|
||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.SlimRole["name"][],
|
||||||
) => void;
|
) => void;
|
||||||
isNonInitialPage: boolean;
|
isNonInitialPage: boolean;
|
||||||
actorID: string;
|
actorID: string;
|
||||||
|
@ -229,27 +229,27 @@ export const MockUpdateCheck: TypesGen.UpdateCheckResponse = {
|
|||||||
version: "v99.999.9999+c9cdf14",
|
version: "v99.999.9999+c9cdf14",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockOwnerRole: TypesGen.Role = {
|
export const MockOwnerRole: TypesGen.SlimRole = {
|
||||||
name: "owner",
|
name: "owner",
|
||||||
display_name: "Owner",
|
display_name: "Owner",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockUserAdminRole: TypesGen.Role = {
|
export const MockUserAdminRole: TypesGen.SlimRole = {
|
||||||
name: "user_admin",
|
name: "user_admin",
|
||||||
display_name: "User Admin",
|
display_name: "User Admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockTemplateAdminRole: TypesGen.Role = {
|
export const MockTemplateAdminRole: TypesGen.SlimRole = {
|
||||||
name: "template_admin",
|
name: "template_admin",
|
||||||
display_name: "Template Admin",
|
display_name: "Template Admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockMemberRole: TypesGen.Role = {
|
export const MockMemberRole: TypesGen.SlimRole = {
|
||||||
name: "member",
|
name: "member",
|
||||||
display_name: "Member",
|
display_name: "Member",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockAuditorRole: TypesGen.Role = {
|
export const MockAuditorRole: TypesGen.SlimRole = {
|
||||||
name: "auditor",
|
name: "auditor",
|
||||||
display_name: "Auditor",
|
display_name: "Auditor",
|
||||||
};
|
};
|
||||||
@ -257,7 +257,7 @@ export const MockAuditorRole: TypesGen.Role = {
|
|||||||
// assignableRole takes a role and a boolean. The boolean implies if the
|
// assignableRole takes a role and a boolean. The boolean implies if the
|
||||||
// actor can assign (add/remove) the role from other users.
|
// actor can assign (add/remove) the role from other users.
|
||||||
export function assignableRole(
|
export function assignableRole(
|
||||||
role: TypesGen.Role,
|
role: TypesGen.SlimRole,
|
||||||
assignable: boolean,
|
assignable: boolean,
|
||||||
): TypesGen.AssignableRoles {
|
): TypesGen.AssignableRoles {
|
||||||
return {
|
return {
|
||||||
|
Reference in New Issue
Block a user