mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +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}": {
|
||||
@ -9547,17 +9573,20 @@ const docTemplate = `{
|
||||
"enum": [
|
||||
"example",
|
||||
"auto-fill-parameters",
|
||||
"multi-organization"
|
||||
"multi-organization",
|
||||
"custom-roles"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"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.",
|
||||
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentAutoFillParameters",
|
||||
"ExperimentMultiOrganization"
|
||||
"ExperimentMultiOrganization",
|
||||
"ExperimentCustomRoles"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@ -10372,7 +10401,7 @@ const docTemplate = `{
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -11094,6 +11138,28 @@ const docTemplate = `{
|
||||
},
|
||||
"name": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -11677,7 +11754,7 @@ const docTemplate = `{
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@ -12214,7 +12291,7 @@ const docTemplate = `{
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"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}": {
|
||||
@ -8545,16 +8567,23 @@
|
||||
},
|
||||
"codersdk.Experiment": {
|
||||
"type": "string",
|
||||
"enum": ["example", "auto-fill-parameters", "multi-organization"],
|
||||
"enum": [
|
||||
"example",
|
||||
"auto-fill-parameters",
|
||||
"multi-organization",
|
||||
"custom-roles"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"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.",
|
||||
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentAutoFillParameters",
|
||||
"ExperimentMultiOrganization"
|
||||
"ExperimentMultiOrganization",
|
||||
"ExperimentCustomRoles"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@ -9316,7 +9345,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": ["callback_url", "name"],
|
||||
@ -9996,6 +10040,28 @@
|
||||
},
|
||||
"name": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -10559,7 +10636,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@ -11053,7 +11130,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
@ -196,12 +196,12 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
CreatedAt: dblog.UserCreatedAt.Time,
|
||||
Status: codersdk.UserStatus(dblog.UserStatus.UserStatus),
|
||||
},
|
||||
Roles: []codersdk.Role{},
|
||||
Roles: []codersdk.SlimRole{},
|
||||
}
|
||||
|
||||
for _, roleName := range dblog.UserRoles {
|
||||
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/rbac"
|
||||
"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/telemetry"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
@ -631,6 +632,7 @@ func New(options *Options) *API {
|
||||
httpmw.AttachRequestID,
|
||||
httpmw.ExtractRealIP(api.RealIPConfig),
|
||||
httpmw.Logger(api.Logger),
|
||||
rolestore.CustomRoleMW,
|
||||
prometheusMW,
|
||||
// Build-Version is helpful for debugging.
|
||||
func(next http.Handler) http.Handler {
|
||||
@ -915,7 +917,7 @@ func New(options *Options) *API {
|
||||
r.Post("/logout", api.postLogout)
|
||||
// These routes query information about site wide roles.
|
||||
r.Route("/roles", func(r chi.Router) {
|
||||
r.Get("/", api.assignableSiteRoles)
|
||||
r.Get("/", api.AssignableSiteRoles)
|
||||
})
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/parameter"
|
||||
"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/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
@ -28,9 +29,25 @@ import (
|
||||
// database types to slices of codersdk types.
|
||||
// Only works if the function takes a single argument.
|
||||
func List[F any, T any](list []F, convert func(F) T) []T {
|
||||
into := make([]T, 0, len(list))
|
||||
for _, item := range list {
|
||||
into = append(into, convert(item))
|
||||
return ListLazy(convert)(list)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -150,12 +167,20 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
||||
convertedUser := codersdk.User{
|
||||
ReducedUser: ReducedUser(user),
|
||||
OrganizationIDs: organizationIDs,
|
||||
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
|
||||
Roles: make([]codersdk.SlimRole, 0, len(user.RBACRoles)),
|
||||
}
|
||||
|
||||
for _, roleName := range user.RBACRoles {
|
||||
rbacRole, _ := rbac.RoleByName(roleName)
|
||||
convertedUser.Roles = append(convertedUser.Roles, Role(rbacRole))
|
||||
rbacRole, err := rbac.RoleByName(roleName)
|
||||
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
|
||||
@ -180,8 +205,8 @@ func Group(group database.Group, members []database.User) codersdk.Group {
|
||||
}
|
||||
}
|
||||
|
||||
func Role(role rbac.Role) codersdk.Role {
|
||||
return codersdk.Role{
|
||||
func SlimRole(role rbac.Role) codersdk.SlimRole {
|
||||
return codersdk.SlimRole{
|
||||
DisplayName: role.DisplayName,
|
||||
Name: role.Name,
|
||||
}
|
||||
@ -500,3 +525,39 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
// returns them all, but then someone could pass in a large list to make us do
|
||||
// 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 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/promoauth"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||
"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 := rbac.Subject{
|
||||
FriendlyName: roles.Username,
|
||||
ID: key.UserID.String(),
|
||||
Roles: rbac.RoleNames(roles.Roles),
|
||||
Roles: rbacRoles,
|
||||
Groups: roles.Groups,
|
||||
Scope: rbac.ScopeName(key.Scope),
|
||||
}.WithCachedASTValue()
|
||||
|
@ -82,7 +82,7 @@ func (api *API) updateOrganizationMemberRoles(ctx context.Context, args database
|
||||
}
|
||||
|
||||
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,
|
||||
CreatedAt: mem.CreatedAt,
|
||||
UpdatedAt: mem.UpdatedAt,
|
||||
Roles: make([]codersdk.Role, 0, len(mem.Roles)),
|
||||
Roles: make([]codersdk.SlimRole, 0, len(mem.Roles)),
|
||||
}
|
||||
|
||||
for _, roleName := range mem.Roles {
|
||||
rbacRole, _ := rbac.RoleByName(roleName)
|
||||
convertedMember.Roles = append(convertedMember.Roles, db2sdk.Role(rbacRole))
|
||||
convertedMember.Roles = append(convertedMember.Roles, db2sdk.SlimRole(rbacRole))
|
||||
}
|
||||
return convertedMember
|
||||
}
|
||||
|
@ -1,14 +1,96 @@
|
||||
package rolestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"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) {
|
||||
role := rbac.Role{
|
||||
Name: dbRole.Name,
|
||||
@ -35,3 +117,30 @@ func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
// @ID get-site-member-roles
|
||||
@ -20,7 +20,7 @@ import (
|
||||
// @Tags Members
|
||||
// @Success 200 {array} codersdk.AssignableRoles
|
||||
// @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()
|
||||
actorRoles := httpmw.UserAuthorization(r)
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
// @ID get-member-roles-by-organization
|
||||
@ -66,7 +66,7 @@ func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role) []coder
|
||||
continue
|
||||
}
|
||||
assignable = append(assignable, codersdk.AssignableRoles{
|
||||
Role: codersdk.Role{
|
||||
SlimRole: codersdk.SlimRole{
|
||||
Name: role.Name,
|
||||
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)
|
||||
return codersdk.Role{
|
||||
return codersdk.SlimRole{
|
||||
DisplayName: role.DisplayName,
|
||||
Name: role.Name,
|
||||
}
|
||||
@ -156,7 +156,7 @@ func convertRoles(assignableRoles map[string]bool) []codersdk.AssignableRoles {
|
||||
for roleName, assignable := range assignableRoles {
|
||||
role := convertRole(roleName)
|
||||
converted = append(converted, codersdk.AssignableRoles{
|
||||
Role: role,
|
||||
SlimRole: role,
|
||||
Assignable: assignable,
|
||||
})
|
||||
}
|
||||
|
@ -1095,7 +1095,7 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := UpdateSiteUserRoles(ctx, api.Database, database.UpdateUserRolesParams{
|
||||
updatedUser, err := api.Database.UpdateUserRoles(ctx, database.UpdateUserRolesParams{
|
||||
GrantedRoles: params.Roles,
|
||||
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))
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// @Summary Get organizations by user
|
||||
|
@ -1049,7 +1049,7 @@ func TestGrantSiteRoles(t *testing.T) {
|
||||
c.AssignToUser = newUser.ID.String()
|
||||
}
|
||||
|
||||
var newRoles []codersdk.Role
|
||||
var newRoles []codersdk.SlimRole
|
||||
if c.OrgID != uuid.Nil {
|
||||
// Org assign
|
||||
var mem codersdk.OrganizationMember
|
||||
|
Reference in New Issue
Block a user