mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add organization scoping to chats (#23827)
Fixes https://github.com/coder/internal/issues/1436 * Adds organization_id to chats with backfill (workspace org → user org membership → default org) * No support yet for ACLs (follow-up issue) - Cross-org workspace binding rejected (both in `CreateChatRequest` and in `create_workspace` tool - Adds `OrganizationAutocomplete` to `AgentCreateForm` - Docs updated with `organization_id` in chats-api.md > 🤖 Written by a Coder Agent. Reviewed by many humans and many agents. --------- Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
This commit is contained in:
@@ -689,11 +689,11 @@ func ConfigWithoutACL() regosql.ConvertConfig {
|
||||
}
|
||||
|
||||
// ConfigChats is the configuration for converting rego to SQL when
|
||||
// the target table is "chats", which has no organization_id or ACL
|
||||
// the target table is "chats", which has no ACL
|
||||
// columns.
|
||||
func ConfigChats() regosql.ConvertConfig {
|
||||
return regosql.ConvertConfig{
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -287,16 +287,15 @@ neq(input.object.owner, "");
|
||||
Queries: []string{
|
||||
`"me" = input.object.owner; input.object.owner != ""; input.object.org_owner = ""`,
|
||||
},
|
||||
ExpectedSQL: p(p("'me' = owner_id :: text") + " AND " + p("owner_id :: text != ''") + " AND " + p("'' = ''")),
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
ExpectedSQL: p(p("'me' = owner_id :: text") + " AND " + p("owner_id :: text != ''") + " AND " + p("organization_id :: text = ''")),
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
},
|
||||
{
|
||||
Name: "ChatOrgScopedNeverMatches",
|
||||
Name: "ChatOrgScopedMatches",
|
||||
Queries: []string{
|
||||
`input.object.org_owner = "org-id"`,
|
||||
},
|
||||
ExpectedSQL: p("'' = 'org-id'"),
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
ExpectedSQL: p("organization_id :: text = 'org-id'"), VariableConverter: regosql.NoACLConverter(),
|
||||
},
|
||||
{
|
||||
Name: "AuditLogUUID",
|
||||
|
||||
@@ -126,30 +126,6 @@ func NoACLConverter() *sqltypes.VariableConverter {
|
||||
return matcher
|
||||
}
|
||||
|
||||
// ChatConverter should be used for the chats table, which has no
|
||||
// organization_id, group_acl, or user_acl columns.
|
||||
func ChatConverter() *sqltypes.VariableConverter {
|
||||
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
// The chats table has no organization_id column. Map org_owner
|
||||
// to a literal empty string so that:
|
||||
// - User-level ownership checks (org_owner = '') activate correctly.
|
||||
// - Org-scoped permissions never match (org_owner will never equal
|
||||
// a real org UUID), which is intentional since chats are not
|
||||
// org-scoped resources.
|
||||
// Note: custom org roles that include "chat" permissions will
|
||||
// silently have no effect because of this mapping.
|
||||
sqltypes.StringVarMatcher("''", []string{"input", "object", "org_owner"}),
|
||||
userOwnerMatcher(),
|
||||
)
|
||||
matcher.RegisterMatcher(
|
||||
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
||||
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
||||
)
|
||||
|
||||
return matcher
|
||||
}
|
||||
|
||||
func DefaultVariableConverter() *sqltypes.VariableConverter {
|
||||
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
|
||||
@@ -437,17 +437,22 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
return auditorRole
|
||||
},
|
||||
|
||||
// templateAdmin grants all actions on templates, files,
|
||||
// provisioner daemons, and prebuilt workspaces.
|
||||
templateAdmin: func(_ uuid.UUID) Role {
|
||||
return templateAdminRole
|
||||
},
|
||||
|
||||
// userAdmin grants all actions on users, groups, roles,
|
||||
// and organization membership.
|
||||
userAdmin: func(_ uuid.UUID) Role {
|
||||
return userAdminRole
|
||||
},
|
||||
|
||||
// agentsAccess grants all actions on chat resources owned
|
||||
// by the user. Without this role, members cannot create
|
||||
// or interact with chats.
|
||||
// by the user. Without this role, members can still read,
|
||||
// update, and delete their own chats via org membership,
|
||||
// but cannot create chats or trigger AI inference.
|
||||
agentsAccess: func(_ uuid.UUID) Role {
|
||||
return agentsAccessRole
|
||||
},
|
||||
|
||||
@@ -205,6 +205,34 @@ func TestRolePermissions(t *testing.T) {
|
||||
orgTemplateAdmin := authSubject{Name: "org_template_admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgTemplateAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
orgAdminBanWorkspace := authSubject{Name: "org_admin_workspace_ban", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID), rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
agentsAccessUser := authSubject{Name: "chat_access", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAgentsAccess()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
orgMemberMe := func() authSubject {
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
perms := rbac.OrgMemberPermissions(rbac.OrgSettings{
|
||||
ShareableWorkspaceOwners: rbac.ShareableWorkspaceOwnersEveryone,
|
||||
})
|
||||
return authSubject{
|
||||
Name: "org_member_me",
|
||||
Actor: rbac.Subject{
|
||||
ID: currentUser.String(),
|
||||
Roles: rbac.Roles{
|
||||
memberRole,
|
||||
{
|
||||
Identifier: rbac.ScopedRoleOrgMember(orgID),
|
||||
Site: []rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
ByOrgID: map[string]rbac.OrgPermissions{
|
||||
orgID.String(): {
|
||||
Org: perms.Org,
|
||||
Member: perms.Member,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue(),
|
||||
}
|
||||
}()
|
||||
setOrgNotMe := authSubjectSet{orgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin}
|
||||
|
||||
otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
@@ -1070,16 +1098,10 @@ func TestRolePermissions(t *testing.T) {
|
||||
{
|
||||
Name: "ChatUsage",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceChat.WithOwner(currentUser.String()),
|
||||
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, agentsAccessUser},
|
||||
false: {
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
},
|
||||
true: {owner, orgAdmin, orgMemberMe},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user