mirror of
https://github.com/coder/coder.git
synced 2026-10-31 11:02:50 +08:00
feat: add chat model config RBAC resource (#27957)
Add the `chat_model_config` RBAC resource, actions, and API key scopes. This layer includes the scope migration and generated API types. Model list queries apply token scopes, SQL authorization filters, and row ACLs. Write and default-management handlers keep deployment configuration authorization until the next API layer replaces it. Depends on #27956 _This pull request description was generated by Coder Agents._
This commit is contained in:
@@ -745,6 +745,14 @@ func ConfigWorkspaces() regosql.ConvertConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigChatModelConfigs uses a resource converter so SQL filters qualify
|
||||
// chat model config columns consistently with GetChatModelConfigs.
|
||||
func ConfigChatModelConfigs() regosql.ConvertConfig {
|
||||
return regosql.ConvertConfig{
|
||||
VariableConverter: regosql.ChatModelConfigConverter(),
|
||||
}
|
||||
}
|
||||
|
||||
func Compile(cfg regosql.ConvertConfig, pa *PartialAuthorizer) (AuthorizeFilter, error) {
|
||||
root, err := regosql.ConvertRegoAst(cfg, pa.partialQueries)
|
||||
if err != nil {
|
||||
|
||||
@@ -128,6 +128,17 @@ var (
|
||||
Type: "chat",
|
||||
}
|
||||
|
||||
// ResourceChatModelConfig
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a new chat model config
|
||||
// - "ActionDelete" :: delete a chat model config
|
||||
// - "ActionRead" :: read chat model configs
|
||||
// - "ActionShare" :: share a chat model config with other users or groups
|
||||
// - "ActionUpdate" :: update a chat model config
|
||||
ResourceChatModelConfig = Object{
|
||||
Type: "chat_model_config",
|
||||
}
|
||||
|
||||
// ResourceConnectionLog
|
||||
// Valid Actions
|
||||
// - "ActionRead" :: read connection logs
|
||||
@@ -522,6 +533,7 @@ func AllResources() []Objecter {
|
||||
ResourceBoundaryLog,
|
||||
ResourceBoundaryUsage,
|
||||
ResourceChat,
|
||||
ResourceChatModelConfig,
|
||||
ResourceConnectionLog,
|
||||
ResourceCryptoKey,
|
||||
ResourceDebugInfo,
|
||||
|
||||
@@ -93,6 +93,14 @@ var mcpServerConfigActions = map[Action]ActionDefinition{
|
||||
ActionDelete: "delete MCP server config",
|
||||
}
|
||||
|
||||
var chatModelConfigActions = map[Action]ActionDefinition{
|
||||
ActionCreate: "create a new chat model config",
|
||||
ActionRead: "read chat model configs",
|
||||
ActionUpdate: "update a chat model config",
|
||||
ActionDelete: "delete a chat model config",
|
||||
ActionShare: "share a chat model config with other users or groups",
|
||||
}
|
||||
|
||||
// RBACPermissions is indexed by the type
|
||||
var RBACPermissions = map[string]PermissionDefinition{
|
||||
// Wildcard is every object, and the action "*" provides all actions.
|
||||
@@ -122,6 +130,9 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
"chat": {
|
||||
Actions: chatActions,
|
||||
},
|
||||
"chat_model_config": {
|
||||
Actions: chatModelConfigActions,
|
||||
},
|
||||
// Dormant workspaces have the same perms as workspaces.
|
||||
"workspace_dormant": {
|
||||
Actions: workspaceActions,
|
||||
|
||||
@@ -227,6 +227,52 @@ func TestRegoQueries(t *testing.T) {
|
||||
" OR (chats_expanded.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? '*'))",
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
},
|
||||
{
|
||||
Name: "UserChatModelConfigACLAllow",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
`"*" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`,
|
||||
},
|
||||
ExpectedSQL: "((cmc.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? 'read')" +
|
||||
" OR (cmc.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? '*'))",
|
||||
VariableConverter: regosql.ChatModelConfigConverter(),
|
||||
},
|
||||
{
|
||||
Name: "GroupChatModelConfigACLAllow",
|
||||
Queries: []string{
|
||||
`"read" in input.object.acl_group_list["96c55a0e-73b4-44fc-abac-70d53c35c04c"]`,
|
||||
`"*" in input.object.acl_group_list["96c55a0e-73b4-44fc-abac-70d53c35c04c"]`,
|
||||
},
|
||||
ExpectedSQL: "((cmc.group_acl#>array['96c55a0e-73b4-44fc-abac-70d53c35c04c', 'permissions'] ? 'read')" +
|
||||
" OR (cmc.group_acl#>array['96c55a0e-73b4-44fc-abac-70d53c35c04c', 'permissions'] ? '*'))",
|
||||
VariableConverter: regosql.ChatModelConfigConverter(),
|
||||
},
|
||||
{
|
||||
Name: "ChatModelConfigOrgScopedMatches",
|
||||
Queries: []string{
|
||||
`input.object.org_owner = "org-id"`,
|
||||
},
|
||||
ExpectedSQL: p("cmc.organization_id :: text = 'org-id'"),
|
||||
VariableConverter: regosql.ChatModelConfigConverter(),
|
||||
},
|
||||
{
|
||||
Name: "ChatModelConfigNoOwner",
|
||||
Queries: []string{
|
||||
`input.object.owner != ""`,
|
||||
},
|
||||
ExpectedSQL: p("false"),
|
||||
VariableConverter: regosql.ChatModelConfigConverter(),
|
||||
},
|
||||
{
|
||||
Name: "ChatModelConfigAllowList",
|
||||
Queries: []string{
|
||||
`input.object.id != ""`,
|
||||
`input.object.id in ["9046b041-58ed-47a3-9c3a-de302577875a"]`,
|
||||
},
|
||||
ExpectedSQL: p(`(cmc.id :: text != '') OR ` +
|
||||
`(cmc.id :: text = ANY(ARRAY ['9046b041-58ed-47a3-9c3a-de302577875a']))`),
|
||||
VariableConverter: regosql.ChatModelConfigConverter(),
|
||||
},
|
||||
{
|
||||
Name: "ChatAllowList",
|
||||
Queries: []string{
|
||||
|
||||
@@ -95,6 +95,23 @@ func chatBaseConverter() *sqltypes.VariableConverter {
|
||||
)
|
||||
}
|
||||
|
||||
// ChatModelConfigConverter qualifies columns against the cmc alias used by
|
||||
// GetChatModelConfigs. Chat model configs have no user owner,
|
||||
// only an organization owner.
|
||||
func ChatModelConfigConverter() *sqltypes.VariableConverter {
|
||||
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
sqltypes.StringVarMatcher("cmc.id :: text", []string{"input", "object", "id"}),
|
||||
sqltypes.StringVarMatcher("cmc.organization_id :: text", []string{"input", "object", "org_owner"}),
|
||||
sqltypes.AlwaysFalse(userOwnerMatcher()),
|
||||
)
|
||||
matcher.RegisterMatcher(
|
||||
ACLMappingMatcher(matcher, "cmc.group_acl", []string{"input", "object", "acl_group_list"}).UsingSubfield("permissions"),
|
||||
ACLMappingMatcher(matcher, "cmc.user_acl", []string{"input", "object", "acl_user_list"}).UsingSubfield("permissions"),
|
||||
)
|
||||
|
||||
return matcher
|
||||
}
|
||||
|
||||
func AuditLogConverter() *sqltypes.VariableConverter {
|
||||
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
|
||||
@@ -476,6 +476,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
// Allow auditors to query deployment stats and insights.
|
||||
ResourceDeploymentStats.Type: {policy.ActionRead},
|
||||
ResourceDeploymentConfig.Type: {policy.ActionRead},
|
||||
ResourceChatModelConfig.Type: {policy.ActionRead},
|
||||
// Allow auditors to query AI Bridge interceptions.
|
||||
ResourceAibridgeInterception.Type: {policy.ActionRead},
|
||||
// Allow auditors to read boundary logs.
|
||||
@@ -611,6 +612,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceGroupMember.Type: {policy.ActionRead},
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
ResourceOrganizationMember.Type: {policy.ActionRead},
|
||||
ResourceChatModelConfig.Type: {policy.ActionRead},
|
||||
}),
|
||||
Member: []Permission{},
|
||||
},
|
||||
|
||||
@@ -1461,6 +1461,35 @@ func TestRolePermissions(t *testing.T) {
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatModelConfigRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceChatModelConfig.WithID(uuid.New()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, auditor, orgAuditor},
|
||||
false: {
|
||||
memberMe, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
otherOrgAdmin, otherOrgAuditor,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatModelConfigCreateUpdateDeleteShare",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionShare},
|
||||
Resource: rbac.ResourceChatModelConfig.WithID(uuid.New()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {
|
||||
memberMe, orgMemberMe, agentsAccessUser, orgWorkspaceAccessUser,
|
||||
auditor, orgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
otherOrgAdmin, otherOrgAuditor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
// Build coverage set from test case definitions statically,
|
||||
// so we don't need shared mutable state during execution.
|
||||
|
||||
@@ -35,6 +35,10 @@ var externalLowLevel = map[ScopeName]struct{}{
|
||||
"api_key:delete": {},
|
||||
"api_key:*": {},
|
||||
|
||||
// Chat model configs
|
||||
"chat_model_config:read": {},
|
||||
"chat_model_config:share": {},
|
||||
|
||||
// Files
|
||||
"file:read": {},
|
||||
"file:create": {},
|
||||
|
||||
@@ -63,6 +63,7 @@ func TestIsExternalScope(t *testing.T) {
|
||||
require.True(t, IsExternalScope("workspace:*"))
|
||||
require.True(t, IsExternalScope("coder:workspaces.create"))
|
||||
require.True(t, IsExternalScope("user:read"))
|
||||
require.True(t, IsExternalScope("chat_model_config:share"))
|
||||
require.False(t, IsExternalScope("debug_info:read")) // internal-only
|
||||
require.False(t, IsExternalScope("unknown:read"))
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ const (
|
||||
ScopeChatRead ScopeName = "chat:read"
|
||||
ScopeChatShare ScopeName = "chat:share"
|
||||
ScopeChatUpdate ScopeName = "chat:update"
|
||||
ScopeChatModelConfigCreate ScopeName = "chat_model_config:create"
|
||||
ScopeChatModelConfigDelete ScopeName = "chat_model_config:delete"
|
||||
ScopeChatModelConfigRead ScopeName = "chat_model_config:read"
|
||||
ScopeChatModelConfigShare ScopeName = "chat_model_config:share"
|
||||
ScopeChatModelConfigUpdate ScopeName = "chat_model_config:update"
|
||||
ScopeConnectionLogRead ScopeName = "connection_log:read"
|
||||
ScopeConnectionLogUpdate ScopeName = "connection_log:update"
|
||||
ScopeCryptoKeyCreate ScopeName = "crypto_key:create"
|
||||
@@ -241,6 +246,11 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeChatRead,
|
||||
ScopeChatShare,
|
||||
ScopeChatUpdate,
|
||||
ScopeChatModelConfigCreate,
|
||||
ScopeChatModelConfigDelete,
|
||||
ScopeChatModelConfigRead,
|
||||
ScopeChatModelConfigShare,
|
||||
ScopeChatModelConfigUpdate,
|
||||
ScopeConnectionLogRead,
|
||||
ScopeConnectionLogUpdate,
|
||||
ScopeCryptoKeyCreate,
|
||||
@@ -435,6 +445,11 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeChatRead,
|
||||
ScopeChatShare,
|
||||
ScopeChatUpdate,
|
||||
ScopeChatModelConfigCreate,
|
||||
ScopeChatModelConfigDelete,
|
||||
ScopeChatModelConfigRead,
|
||||
ScopeChatModelConfigShare,
|
||||
ScopeChatModelConfigUpdate,
|
||||
ScopeConnectionLogRead,
|
||||
ScopeConnectionLogUpdate,
|
||||
ScopeCryptoKeyCreate,
|
||||
|
||||
Reference in New Issue
Block a user