From 5334fb4dc618fc7034f66f9b91300e01d9c31115 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 29 Jul 2025 22:14:03 +0000 Subject: [PATCH 1/9] rego updates --- coderd/rbac/regosql/acl_group_var.go | 60 ++++++++++++++++++---------- coderd/rbac/regosql/compile_test.go | 20 ++++++++++ coderd/rbac/regosql/configs.go | 8 ++-- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/coderd/rbac/regosql/acl_group_var.go b/coderd/rbac/regosql/acl_group_var.go index 328dfbcd48d0a..24d72b8f6629a 100644 --- a/coderd/rbac/regosql/acl_group_var.go +++ b/coderd/rbac/regosql/acl_group_var.go @@ -11,11 +11,11 @@ import ( ) var ( - _ sqltypes.VariableMatcher = ACLGroupVar{} - _ sqltypes.Node = ACLGroupVar{} + _ sqltypes.VariableMatcher = ACLMappingVar{} + _ sqltypes.Node = ACLMappingVar{} ) -// ACLGroupVar is a variable matcher that handles group_acl and user_acl. +// ACLMappingVar is a variable matcher that handles group_acl and user_acl. // The sql type is a jsonb object with the following structure: // // "group_acl": { @@ -23,28 +23,41 @@ var ( // } // // This is a custom variable matcher as json objects have arbitrary complexity. -type ACLGroupVar struct { - StructSQL string - // input.object.group_acl -> ["input", "object", "group_acl"] - StructPath []string - - // FieldReference handles referencing the subfields, which could be - // more variables. We pass one in as the global one might not be correctly - // scoped. +type ACLMappingVar struct { + // SelectSQL is used to select the ACL mapping from the table for the + // given resource. ie. if the full query might look like `SELECT group_acl + // FROM things;` then you would want this to be `"group_acl"`. + SelectSQL string + // FieldReference handles variable references when indexing into the mapping. + // (like `input.object.acl_group_list[input.object.group_id]`). We pass one in + // as the global one might not be correctly scoped. FieldReference sqltypes.VariableMatcher + // Used if the action list isn't directly in the ACL entry. For example, in + // the `workspaces.group_acl` and `workspaces.user_acl` columns they're stored + // under a `"roles"` key. + Subfield string + + // StructPath represents the path of the value in rego + // ie. input.object.group_acl -> ["input", "object", "group_acl"] + StructPath []string // Instance fields Source sqltypes.RegoSource GroupNode sqltypes.Node } -func ACLGroupMatcher(fieldReference sqltypes.VariableMatcher, structSQL string, structPath []string) ACLGroupVar { - return ACLGroupVar{StructSQL: structSQL, StructPath: structPath, FieldReference: fieldReference} +func ACLGroupMatcher(fieldReference sqltypes.VariableMatcher, structSQL string, structPath []string) ACLMappingVar { + return ACLMappingVar{SelectSQL: structSQL, StructPath: structPath, FieldReference: fieldReference} } -func (ACLGroupVar) UseAs() sqltypes.Node { return ACLGroupVar{} } +func (g ACLMappingVar) UsingSubfield(subfield string) ACLMappingVar { + g.Subfield = subfield + return g +} + +func (ACLMappingVar) UseAs() sqltypes.Node { return ACLMappingVar{} } -func (g ACLGroupVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { +func (g ACLMappingVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { // "left" will be a map of group names to actions in rego. // { // "all_users": ["read"] @@ -54,10 +67,12 @@ func (g ACLGroupVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { return nil, false } - aclGrp := ACLGroupVar{ - StructSQL: g.StructSQL, - StructPath: g.StructPath, + aclGrp := ACLMappingVar{ + SelectSQL: g.SelectSQL, FieldReference: g.FieldReference, + Subfield: g.Subfield, + + StructPath: g.StructPath, Source: sqltypes.RegoSource(rego.String()), } @@ -89,11 +104,14 @@ func (g ACLGroupVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { return nil, false } -func (g ACLGroupVar) SQLString(cfg *sqltypes.SQLGenerator) string { - return fmt.Sprintf("%s->%s", g.StructSQL, g.GroupNode.SQLString(cfg)) +func (g ACLMappingVar) SQLString(cfg *sqltypes.SQLGenerator) string { + if g.Subfield != "" { + return fmt.Sprintf("%s->%s->%s", g.SelectSQL, g.GroupNode.SQLString(cfg), g.Subfield) + } + return fmt.Sprintf("%s->%s", g.SelectSQL, g.GroupNode.SQLString(cfg)) } -func (g ACLGroupVar) ContainsSQL(cfg *sqltypes.SQLGenerator, other sqltypes.Node) (string, error) { +func (g ACLMappingVar) ContainsSQL(cfg *sqltypes.SQLGenerator, other sqltypes.Node) (string, error) { switch other.UseAs().(type) { // Only supports containing other strings. case sqltypes.AstString: diff --git a/coderd/rbac/regosql/compile_test.go b/coderd/rbac/regosql/compile_test.go index 07e8e7245a53e..12580fbeab29c 100644 --- a/coderd/rbac/regosql/compile_test.go +++ b/coderd/rbac/regosql/compile_test.go @@ -197,6 +197,26 @@ func TestRegoQueries(t *testing.T) { "(user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? '*'))", VariableConverter: regosql.DefaultVariableConverter(), }, + { + Name: "UserWorkspaceACLAllow", + 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: "((user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? 'read') OR " + + "(user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? '*'))", + VariableConverter: regosql.WorkspaceConverter(), + }, + { + Name: "GroupWorkspaceACLAllow", + Queries: []string{ + `"read" in input.object.acl_group_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, + `"*" in input.object.acl_group_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, + }, + ExpectedSQL: "((group_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? 'read') OR " + + "(group_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? '*'))", + VariableConverter: regosql.WorkspaceConverter(), + }, { Name: "NoACLConfig", Queries: []string{ diff --git a/coderd/rbac/regosql/configs.go b/coderd/rbac/regosql/configs.go index 69d425d9dba2f..1f86c6839dc6c 100644 --- a/coderd/rbac/regosql/configs.go +++ b/coderd/rbac/regosql/configs.go @@ -14,11 +14,11 @@ func userOwnerMatcher() sqltypes.VariableMatcher { return sqltypes.StringVarMatcher("owner_id :: text", []string{"input", "object", "owner"}) } -func groupACLMatcher(m sqltypes.VariableMatcher) sqltypes.VariableMatcher { +func groupACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar { return ACLGroupMatcher(m, "group_acl", []string{"input", "object", "acl_group_list"}) } -func userACLMatcher(m sqltypes.VariableMatcher) sqltypes.VariableMatcher { +func userACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar { return ACLGroupMatcher(m, "user_acl", []string{"input", "object", "acl_user_list"}) } @@ -88,8 +88,8 @@ func WorkspaceConverter() *sqltypes.VariableConverter { userOwnerMatcher(), ) matcher.RegisterMatcher( - sqltypes.AlwaysFalse(groupACLMatcher(matcher)), - sqltypes.AlwaysFalse(userACLMatcher(matcher)), + groupACLMatcher(matcher).UsingSubfield("roles"), + userACLMatcher(matcher).UsingSubfield("roles"), ) return matcher From 41ad74913fbded26a59cbb597cfabd07d1d05dd3 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 29 Jul 2025 22:21:04 +0000 Subject: [PATCH 2/9] migrations --- .../migrations/000353_workspace_acl.down.sql | 38 +++++++++++++++++ .../migrations/000353_workspace_acl.up.sql | 41 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 coderd/database/migrations/000353_workspace_acl.down.sql create mode 100644 coderd/database/migrations/000353_workspace_acl.up.sql diff --git a/coderd/database/migrations/000353_workspace_acl.down.sql b/coderd/database/migrations/000353_workspace_acl.down.sql new file mode 100644 index 0000000000000..99ce6d7f0014a --- /dev/null +++ b/coderd/database/migrations/000353_workspace_acl.down.sql @@ -0,0 +1,38 @@ +DROP VIEW workspaces_expanded; + +ALTER TABLE workspaces + DROP COLUMN group_acl, + DROP COLUMN user_acl; + +CREATE VIEW workspaces_expanded AS + SELECT workspaces.id, + workspaces.created_at, + workspaces.updated_at, + workspaces.owner_id, + workspaces.organization_id, + workspaces.template_id, + workspaces.deleted, + workspaces.name, + workspaces.autostart_schedule, + workspaces.ttl, + workspaces.last_used_at, + workspaces.dormant_at, + workspaces.deleting_at, + workspaces.automatic_updates, + workspaces.favorite, + workspaces.next_start_at, + visible_users.avatar_url AS owner_avatar_url, + visible_users.username AS owner_username, + visible_users.name AS owner_name, + organizations.name AS organization_name, + organizations.display_name AS organization_display_name, + organizations.icon AS organization_icon, + organizations.description AS organization_description, + templates.name AS template_name, + templates.display_name AS template_display_name, + templates.icon AS template_icon, + templates.description AS template_description + FROM (((workspaces + JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) + JOIN organizations ON ((workspaces.organization_id = organizations.id))) + JOIN templates ON ((workspaces.template_id = templates.id))); diff --git a/coderd/database/migrations/000353_workspace_acl.up.sql b/coderd/database/migrations/000353_workspace_acl.up.sql new file mode 100644 index 0000000000000..7d0449d7dc8ba --- /dev/null +++ b/coderd/database/migrations/000353_workspace_acl.up.sql @@ -0,0 +1,41 @@ +DROP VIEW workspaces_expanded; + +ALTER TABLE workspaces + ADD COLUMN group_acl jsonb not null default '{}'::jsonb, + ADD COLUMN user_acl jsonb not null default '{}'::jsonb; + +-- Recreate the view, now including the new columns +CREATE VIEW workspaces_expanded AS + SELECT workspaces.id, + workspaces.created_at, + workspaces.updated_at, + workspaces.owner_id, + workspaces.organization_id, + workspaces.template_id, + workspaces.deleted, + workspaces.name, + workspaces.autostart_schedule, + workspaces.ttl, + workspaces.last_used_at, + workspaces.dormant_at, + workspaces.deleting_at, + workspaces.automatic_updates, + workspaces.favorite, + workspaces.next_start_at, + workspaces.group_acl, + workspaces.user_acl, + visible_users.avatar_url AS owner_avatar_url, + visible_users.username AS owner_username, + visible_users.name AS owner_name, + organizations.name AS organization_name, + organizations.display_name AS organization_display_name, + organizations.icon AS organization_icon, + organizations.description AS organization_description, + templates.name AS template_name, + templates.display_name AS template_display_name, + templates.icon AS template_icon, + templates.description AS template_description + FROM (((workspaces + JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) + JOIN organizations ON ((workspaces.organization_id = organizations.id))) + JOIN templates ON ((workspaces.template_id = templates.id))); From 198b8356da877e1c8b1a7917773ce49357f1b924 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 29 Jul 2025 22:30:54 +0000 Subject: [PATCH 3/9] rename the helper too --- coderd/rbac/regosql/acl_group_var.go | 2 +- coderd/rbac/regosql/configs.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/rbac/regosql/acl_group_var.go b/coderd/rbac/regosql/acl_group_var.go index 24d72b8f6629a..83bbd033ac89c 100644 --- a/coderd/rbac/regosql/acl_group_var.go +++ b/coderd/rbac/regosql/acl_group_var.go @@ -46,7 +46,7 @@ type ACLMappingVar struct { GroupNode sqltypes.Node } -func ACLGroupMatcher(fieldReference sqltypes.VariableMatcher, structSQL string, structPath []string) ACLMappingVar { +func ACLMappingMatcher(fieldReference sqltypes.VariableMatcher, structSQL string, structPath []string) ACLMappingVar { return ACLMappingVar{SelectSQL: structSQL, StructPath: structPath, FieldReference: fieldReference} } diff --git a/coderd/rbac/regosql/configs.go b/coderd/rbac/regosql/configs.go index 1f86c6839dc6c..2c55e38d4339c 100644 --- a/coderd/rbac/regosql/configs.go +++ b/coderd/rbac/regosql/configs.go @@ -15,11 +15,11 @@ func userOwnerMatcher() sqltypes.VariableMatcher { } func groupACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar { - return ACLGroupMatcher(m, "group_acl", []string{"input", "object", "acl_group_list"}) + return ACLMappingMatcher(m, "group_acl", []string{"input", "object", "acl_group_list"}) } func userACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar { - return ACLGroupMatcher(m, "user_acl", []string{"input", "object", "acl_user_list"}) + return ACLMappingMatcher(m, "user_acl", []string{"input", "object", "acl_user_list"}) } func TemplateConverter() *sqltypes.VariableConverter { From f9c62742f00e9a08915aee4ef1a756a0baf45b5e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 29 Jul 2025 22:43:34 +0000 Subject: [PATCH 4/9] rename subfield --- coderd/rbac/regosql/acl_group_var.go | 27 ++++++++++++++------------- coderd/rbac/regosql/configs.go | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/coderd/rbac/regosql/acl_group_var.go b/coderd/rbac/regosql/acl_group_var.go index 83bbd033ac89c..1ff63ee3a779d 100644 --- a/coderd/rbac/regosql/acl_group_var.go +++ b/coderd/rbac/regosql/acl_group_var.go @@ -24,17 +24,18 @@ var ( // // This is a custom variable matcher as json objects have arbitrary complexity. type ACLMappingVar struct { - // SelectSQL is used to select the ACL mapping from the table for the + // SelectSQL is used to `SELECT` the ACL mapping from the table for the // given resource. ie. if the full query might look like `SELECT group_acl // FROM things;` then you would want this to be `"group_acl"`. SelectSQL string - // FieldReference handles variable references when indexing into the mapping. - // (like `input.object.acl_group_list[input.object.group_id]`). We pass one in - // as the global one might not be correctly scoped. - FieldReference sqltypes.VariableMatcher + // IndexMatcher handles variable references when indexing into the mapping. + // (ie. `input.object.acl_group_list[input.object.org_owner]`). We need one + // from the local context because the global one might not be correctly + // scoped. + IndexMatcher sqltypes.VariableMatcher // Used if the action list isn't directly in the ACL entry. For example, in // the `workspaces.group_acl` and `workspaces.user_acl` columns they're stored - // under a `"roles"` key. + // under a `"permissions"` key. Subfield string // StructPath represents the path of the value in rego @@ -46,8 +47,8 @@ type ACLMappingVar struct { GroupNode sqltypes.Node } -func ACLMappingMatcher(fieldReference sqltypes.VariableMatcher, structSQL string, structPath []string) ACLMappingVar { - return ACLMappingVar{SelectSQL: structSQL, StructPath: structPath, FieldReference: fieldReference} +func ACLMappingMatcher(indexMatcher sqltypes.VariableMatcher, selectSQL string, structPath []string) ACLMappingVar { + return ACLMappingVar{IndexMatcher: indexMatcher, SelectSQL: selectSQL, StructPath: structPath} } func (g ACLMappingVar) UsingSubfield(subfield string) ACLMappingVar { @@ -68,9 +69,9 @@ func (g ACLMappingVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { } aclGrp := ACLMappingVar{ - SelectSQL: g.SelectSQL, - FieldReference: g.FieldReference, - Subfield: g.Subfield, + SelectSQL: g.SelectSQL, + IndexMatcher: g.IndexMatcher, + Subfield: g.Subfield, StructPath: g.StructPath, @@ -85,8 +86,8 @@ func (g ACLMappingVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { // If the remaining is a variable, then we need to convert it. // Assuming we support variable fields. ref, ok := left[0].Value.(ast.Ref) - if ok && g.FieldReference != nil { - groupNode, ok := g.FieldReference.ConvertVariable(ref) + if ok && g.IndexMatcher != nil { + groupNode, ok := g.IndexMatcher.ConvertVariable(ref) if ok { aclGrp.GroupNode = groupNode return aclGrp, true diff --git a/coderd/rbac/regosql/configs.go b/coderd/rbac/regosql/configs.go index 2c55e38d4339c..cc61bb9b5605a 100644 --- a/coderd/rbac/regosql/configs.go +++ b/coderd/rbac/regosql/configs.go @@ -36,6 +36,20 @@ func TemplateConverter() *sqltypes.VariableConverter { return matcher } +func WorkspaceConverter() *sqltypes.VariableConverter { + matcher := sqltypes.NewVariableConverter().RegisterMatcher( + resourceIDMatcher(), + sqltypes.StringVarMatcher("workspaces.organization_id :: text", []string{"input", "object", "org_owner"}), + userOwnerMatcher(), + ) + matcher.RegisterMatcher( + groupACLMatcher(matcher).UsingSubfield("permissions"), + userACLMatcher(matcher).UsingSubfield("permissions"), + ) + + return matcher +} + func AuditLogConverter() *sqltypes.VariableConverter { matcher := sqltypes.NewVariableConverter().RegisterMatcher( resourceIDMatcher(), @@ -81,20 +95,6 @@ func UserConverter() *sqltypes.VariableConverter { return matcher } -func WorkspaceConverter() *sqltypes.VariableConverter { - matcher := sqltypes.NewVariableConverter().RegisterMatcher( - resourceIDMatcher(), - sqltypes.StringVarMatcher("workspaces.organization_id :: text", []string{"input", "object", "org_owner"}), - userOwnerMatcher(), - ) - matcher.RegisterMatcher( - groupACLMatcher(matcher).UsingSubfield("roles"), - userACLMatcher(matcher).UsingSubfield("roles"), - ) - - return matcher -} - // NoACLConverter should be used when the target SQL table does not contain // group or user ACL columns. func NoACLConverter() *sqltypes.VariableConverter { From d5681695c3c935d264dd46668e161355032fd521 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 29 Jul 2025 23:40:57 +0000 Subject: [PATCH 5/9] fix up queries, make gen --- coderd/database/dump.sql | 6 +- .../migrations/000353_workspace_acl.down.sql | 2 + .../migrations/000353_workspace_acl.up.sql | 2 + coderd/database/modelqueries.go | 2 + coderd/database/models.go | 8 ++- coderd/database/queries.sql.go | 58 ++++++++++++++----- coderd/database/queries/workspaces.sql | 2 + docs/admin/security/audit-logs.md | 2 +- enterprise/audit/table.go | 2 + 9 files changed, 65 insertions(+), 19 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 67d58ad05c802..63e747f7106bf 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2255,7 +2255,9 @@ CREATE TABLE workspaces ( deleting_at timestamp with time zone, automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL, favorite boolean DEFAULT false NOT NULL, - next_start_at timestamp with time zone + next_start_at timestamp with time zone, + group_acl jsonb DEFAULT '{}'::jsonb NOT NULL, + user_acl jsonb DEFAULT '{}'::jsonb NOT NULL ); COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.'; @@ -2434,6 +2436,8 @@ CREATE VIEW workspaces_expanded AS workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, + workspaces.group_acl, + workspaces.user_acl, visible_users.avatar_url AS owner_avatar_url, visible_users.username AS owner_username, visible_users.name AS owner_name, diff --git a/coderd/database/migrations/000353_workspace_acl.down.sql b/coderd/database/migrations/000353_workspace_acl.down.sql index 99ce6d7f0014a..97f0acc6b03c8 100644 --- a/coderd/database/migrations/000353_workspace_acl.down.sql +++ b/coderd/database/migrations/000353_workspace_acl.down.sql @@ -36,3 +36,5 @@ CREATE VIEW workspaces_expanded AS JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) JOIN organizations ON ((workspaces.organization_id = organizations.id))) JOIN templates ON ((workspaces.template_id = templates.id))); + +COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/migrations/000353_workspace_acl.up.sql b/coderd/database/migrations/000353_workspace_acl.up.sql index 7d0449d7dc8ba..6d6a375679aa5 100644 --- a/coderd/database/migrations/000353_workspace_acl.up.sql +++ b/coderd/database/migrations/000353_workspace_acl.up.sql @@ -39,3 +39,5 @@ CREATE VIEW workspaces_expanded AS JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) JOIN organizations ON ((workspaces.organization_id = organizations.id))) JOIN templates ON ((workspaces.template_id = templates.id))); + +COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 6bb7483847a2e..0785c1290d1cd 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -297,6 +297,8 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, diff --git a/coderd/database/models.go b/coderd/database/models.go index 094bc98c68373..734c9d07ee4f8 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3791,6 +3791,8 @@ type Workspace struct { AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` Favorite bool `db:"favorite" json:"favorite"` NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` + UserACL json.RawMessage `db:"user_acl" json:"user_acl"` OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"` OwnerUsername string `db:"owner_username" json:"owner_username"` OwnerName string `db:"owner_name" json:"owner_name"` @@ -4210,6 +4212,8 @@ type WorkspaceTable struct { DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` // Favorite is true if the workspace owner has favorited the workspace. - Favorite bool `db:"favorite" json:"favorite"` - NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + Favorite bool `db:"favorite" json:"favorite"` + NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` + UserACL json.RawMessage `db:"user_acl" json:"user_acl"` } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 80357b3731874..ec2f4e6b243af 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15372,7 +15372,7 @@ func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UU const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted, workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.ai_task_sidebar_app_id, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name FROM @@ -15434,6 +15434,8 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont &i.WorkspaceTable.AutomaticUpdates, &i.WorkspaceTable.Favorite, &i.WorkspaceTable.NextStartAt, + &i.WorkspaceTable.GroupACL, + &i.WorkspaceTable.UserACL, &i.WorkspaceAgent.ID, &i.WorkspaceAgent.CreatedAt, &i.WorkspaceAgent.UpdatedAt, @@ -19524,7 +19526,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description FROM workspaces_expanded as workspaces WHERE @@ -19572,6 +19574,8 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, @@ -19589,7 +19593,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI const getWorkspaceByID = `-- name: GetWorkspaceByID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description FROM workspaces_expanded WHERE @@ -19618,6 +19622,8 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, @@ -19635,7 +19641,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description FROM workspaces_expanded as workspaces WHERE @@ -19671,6 +19677,8 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, @@ -19688,7 +19696,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo const getWorkspaceByResourceID = `-- name: GetWorkspaceByResourceID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description FROM workspaces_expanded as workspaces WHERE @@ -19731,6 +19739,8 @@ func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uu &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, @@ -19748,7 +19758,7 @@ func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uu const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description FROM workspaces_expanded as workspaces WHERE @@ -19803,6 +19813,8 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, @@ -19863,7 +19875,7 @@ SELECT ), filtered_workspaces AS ( SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, latest_build.template_version_id, latest_build.template_version_name, latest_build.completed_at as latest_build_completed_at, @@ -20128,7 +20140,7 @@ WHERE -- @authorize_filter ), filtered_workspaces_order AS ( SELECT - fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_ai_task + fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_ai_task FROM filtered_workspaces fw ORDER BY @@ -20149,7 +20161,7 @@ WHERE $21 ), filtered_workspaces_order_with_summary AS ( SELECT - fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_ai_task + fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_ai_task FROM filtered_workspaces_order fwo -- Return a technical summary row with total count of workspaces. @@ -20172,6 +20184,8 @@ WHERE 'never'::automatic_updates, -- automatic_updates false, -- favorite '0001-01-01 00:00:00+00'::timestamptz, -- next_start_at + '{}'::jsonb, -- group_acl + '{}'::jsonb, -- user_acl '', -- owner_avatar_url '', -- owner_username '', -- owner_name @@ -20201,7 +20215,7 @@ WHERE filtered_workspaces ) SELECT - fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_ai_task, + fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_ai_task, tc.count FROM filtered_workspaces_order_with_summary fwos @@ -20252,6 +20266,8 @@ type GetWorkspacesRow struct { AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` Favorite bool `db:"favorite" json:"favorite"` NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` + UserACL json.RawMessage `db:"user_acl" json:"user_acl"` OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"` OwnerUsername string `db:"owner_username" json:"owner_username"` OwnerName string `db:"owner_name" json:"owner_name"` @@ -20327,6 +20343,8 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, &i.OwnerAvatarUrl, &i.OwnerUsername, &i.OwnerName, @@ -20441,7 +20459,7 @@ func (q *sqlQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerI } const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many -SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at FROM workspaces WHERE template_id = $1 AND deleted = false +SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl FROM workspaces WHERE template_id = $1 AND deleted = false ` func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) { @@ -20470,6 +20488,8 @@ func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, templateID u &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, ); err != nil { return nil, err } @@ -20657,7 +20677,7 @@ INSERT INTO next_start_at ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl ` type InsertWorkspaceParams struct { @@ -20708,6 +20728,8 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, ) return i, err } @@ -20747,7 +20769,7 @@ SET WHERE id = $1 AND deleted = false -RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at +RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl ` type UpdateWorkspaceParams struct { @@ -20775,6 +20797,8 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, ) return i, err } @@ -20863,7 +20887,7 @@ WHERE workspaces.id = $1 AND templates.id = workspaces.template_id RETURNING - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl ` type UpdateWorkspaceDormantDeletingAtParams struct { @@ -20891,6 +20915,8 @@ func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg U &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, ) return i, err } @@ -20965,7 +20991,7 @@ WHERE template_id = $3 AND dormant_at IS NOT NULL -RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at +RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl ` type UpdateWorkspacesDormantDeletingAtByTemplateIDParams struct { @@ -21000,6 +21026,8 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C &i.AutomaticUpdates, &i.Favorite, &i.NextStartAt, + &i.GroupACL, + &i.UserACL, ); err != nil { return nil, err } diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index f166d16f742cd..783cbc56e488c 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -418,6 +418,8 @@ WHERE 'never'::automatic_updates, -- automatic_updates false, -- favorite '0001-01-01 00:00:00+00'::timestamptz, -- next_start_at + '{}'::jsonb, -- group_acl + '{}'::jsonb, -- user_acl '', -- owner_avatar_url '', -- owner_username '', -- owner_name diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 9aca854e46b85..eee0239f01992 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -37,7 +37,7 @@ We track the following resources: | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | WorkspaceBuild
start, stop | |
FieldTracked
ai_task_sidebar_app_idfalse
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| -| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 6c1f907abfa00..6a80f5c212868 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -172,6 +172,8 @@ var auditableResourcesTypes = map[any]map[string]Action{ "automatic_updates": ActionTrack, "favorite": ActionTrack, "next_start_at": ActionTrack, + "group_acl": ActionTrack, + "user_acl": ActionTrack, }, &database.WorkspaceBuild{}: { "id": ActionIgnore, From 0155cf1f8ed31b6e863017595a48c003597a9ccf Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 30 Jul 2025 01:12:30 +0000 Subject: [PATCH 6/9] bandit successfully steals a workspace arc --- .../{acl_group_var.go => acl_mapping_var.go} | 5 ++++- coderd/rbac/regosql/compile_test.go | 16 ++++++++-------- coderd/rbac/regosql/configs.go | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) rename coderd/rbac/regosql/{acl_group_var.go => acl_mapping_var.go} (93%) diff --git a/coderd/rbac/regosql/acl_group_var.go b/coderd/rbac/regosql/acl_mapping_var.go similarity index 93% rename from coderd/rbac/regosql/acl_group_var.go rename to coderd/rbac/regosql/acl_mapping_var.go index 1ff63ee3a779d..c046d867b5aeb 100644 --- a/coderd/rbac/regosql/acl_group_var.go +++ b/coderd/rbac/regosql/acl_mapping_var.go @@ -107,7 +107,10 @@ func (g ACLMappingVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { func (g ACLMappingVar) SQLString(cfg *sqltypes.SQLGenerator) string { if g.Subfield != "" { - return fmt.Sprintf("%s->%s->%s", g.SelectSQL, g.GroupNode.SQLString(cfg), g.Subfield) + // We can't use subsequent -> operators because the first one might return + // NULL, which would result in an error like "column does not exist"' from + // the second. + return fmt.Sprintf("%s#>array[%s, '%s']", g.SelectSQL, g.GroupNode.SQLString(cfg), g.Subfield) } return fmt.Sprintf("%s->%s", g.SelectSQL, g.GroupNode.SQLString(cfg)) } diff --git a/coderd/rbac/regosql/compile_test.go b/coderd/rbac/regosql/compile_test.go index 12580fbeab29c..7bea7f76fd485 100644 --- a/coderd/rbac/regosql/compile_test.go +++ b/coderd/rbac/regosql/compile_test.go @@ -193,8 +193,8 @@ func TestRegoQueries(t *testing.T) { `"read" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, `"*" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, }, - ExpectedSQL: "((user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? 'read') OR " + - "(user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? '*'))", + ExpectedSQL: "((user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? 'read')" + + " OR (user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1' ? '*'))", VariableConverter: regosql.DefaultVariableConverter(), }, { @@ -203,18 +203,18 @@ func TestRegoQueries(t *testing.T) { `"read" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, `"*" in input.object.acl_user_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, }, - ExpectedSQL: "((user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? 'read') OR " + - "(user_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? '*'))", + ExpectedSQL: "((workspaces.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? 'read')" + + " OR (workspaces.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? '*'))", VariableConverter: regosql.WorkspaceConverter(), }, { Name: "GroupWorkspaceACLAllow", Queries: []string{ - `"read" in input.object.acl_group_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, - `"*" in input.object.acl_group_list["d5389ccc-57a4-4b13-8c3f-31747bcdc9f1"]`, + `"read" in input.object.acl_group_list["96c55a0e-73b4-44fc-abac-70d53c35c04c"]`, + `"*" in input.object.acl_group_list["96c55a0e-73b4-44fc-abac-70d53c35c04c"]`, }, - ExpectedSQL: "((group_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? 'read') OR " + - "(group_acl->'d5389ccc-57a4-4b13-8c3f-31747bcdc9f1'->roles ? '*'))", + ExpectedSQL: "((workspaces.group_acl#>array['96c55a0e-73b4-44fc-abac-70d53c35c04c', 'permissions'] ? 'read')" + + " OR (workspaces.group_acl#>array['96c55a0e-73b4-44fc-abac-70d53c35c04c', 'permissions'] ? '*'))", VariableConverter: regosql.WorkspaceConverter(), }, { diff --git a/coderd/rbac/regosql/configs.go b/coderd/rbac/regosql/configs.go index cc61bb9b5605a..1c1e126ff692e 100644 --- a/coderd/rbac/regosql/configs.go +++ b/coderd/rbac/regosql/configs.go @@ -43,8 +43,8 @@ func WorkspaceConverter() *sqltypes.VariableConverter { userOwnerMatcher(), ) matcher.RegisterMatcher( - groupACLMatcher(matcher).UsingSubfield("permissions"), - userACLMatcher(matcher).UsingSubfield("permissions"), + ACLMappingMatcher(matcher, "workspaces.group_acl", []string{"input", "object", "acl_group_list"}).UsingSubfield("permissions"), + ACLMappingMatcher(matcher, "workspaces.user_acl", []string{"input", "object", "acl_user_list"}).UsingSubfield("permissions"), ) return matcher From a20cd4f78de5d502fbf12f1aaf6d8bd66feb6e2f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 30 Jul 2025 20:49:42 +0000 Subject: [PATCH 7/9] add `database.WorkspaceACL` type --- coderd/database/dbgen/dbgen_test.go | 2 ++ coderd/database/models.go | 12 ++++++------ coderd/database/sqlc.yaml | 12 ++++++++++++ coderd/database/types.go | 22 ++++++++++++++++++++++ coderd/rbac/regosql/acl_mapping_var.go | 6 +++--- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/coderd/database/dbgen/dbgen_test.go b/coderd/database/dbgen/dbgen_test.go index 7653176da8079..872704fa1dce0 100644 --- a/coderd/database/dbgen/dbgen_test.go +++ b/coderd/database/dbgen/dbgen_test.go @@ -168,6 +168,8 @@ func TestGenerator(t *testing.T) { DeletingAt: w.DeletingAt, AutomaticUpdates: w.AutomaticUpdates, Favorite: w.Favorite, + GroupACL: database.WorkspaceACL{}, + UserACL: database.WorkspaceACL{}, } require.Equal(t, exp, table) }) diff --git a/coderd/database/models.go b/coderd/database/models.go index 734c9d07ee4f8..62c775c1e7935 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3791,8 +3791,8 @@ type Workspace struct { AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` Favorite bool `db:"favorite" json:"favorite"` NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` - GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` - UserACL json.RawMessage `db:"user_acl" json:"user_acl"` + GroupACL WorkspaceACL `db:"group_acl" json:"group_acl"` + UserACL WorkspaceACL `db:"user_acl" json:"user_acl"` OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"` OwnerUsername string `db:"owner_username" json:"owner_username"` OwnerName string `db:"owner_name" json:"owner_name"` @@ -4212,8 +4212,8 @@ type WorkspaceTable struct { DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` // Favorite is true if the workspace owner has favorited the workspace. - Favorite bool `db:"favorite" json:"favorite"` - NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` - GroupACL json.RawMessage `db:"group_acl" json:"group_acl"` - UserACL json.RawMessage `db:"user_acl" json:"user_acl"` + Favorite bool `db:"favorite" json:"favorite"` + NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + GroupACL WorkspaceACL `db:"group_acl" json:"group_acl"` + UserACL WorkspaceACL `db:"user_acl" json:"user_acl"` } diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index b96dabd1fc187..22dddc2812ed3 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -73,6 +73,18 @@ sql: - column: "template_usage_stats.app_usage_mins" go_type: type: "StringMapOfInt" + - column: "workspaces.user_acl" + go_type: + type: "WorkspaceACL" + - column: "workspaces.group_acl" + go_type: + type: "WorkspaceACL" + - column: "workspaces_expanded.user_acl" + go_type: + type: "WorkspaceACL" + - column: "workspaces_expanded.group_acl" + go_type: + type: "WorkspaceACL" - column: "notification_templates.actions" go_type: type: "[]byte" diff --git a/coderd/database/types.go b/coderd/database/types.go index 6d0f036fe692c..11a0613965b8d 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -77,6 +77,28 @@ func (t TemplateACL) Value() (driver.Value, error) { return json.Marshal(t) } +type WorkspaceACL map[string]WorkspaceACLEntry + +func (t *WorkspaceACL) Scan(src interface{}) error { + switch v := src.(type) { + case string: + return json.Unmarshal([]byte(v), &t) + case []byte, json.RawMessage: + //nolint + return json.Unmarshal(v.([]byte), &t) + } + + return xerrors.Errorf("unexpected type %T", src) +} + +func (t WorkspaceACL) Value() (driver.Value, error) { + return json.Marshal(t) +} + +type WorkspaceACLEntry struct { + Permissions []policy.Action `json:"permissions"` +} + type ExternalAuthProvider struct { ID string `json:"id"` Optional bool `json:"optional,omitempty"` diff --git a/coderd/rbac/regosql/acl_mapping_var.go b/coderd/rbac/regosql/acl_mapping_var.go index c046d867b5aeb..172ac4cc56915 100644 --- a/coderd/rbac/regosql/acl_mapping_var.go +++ b/coderd/rbac/regosql/acl_mapping_var.go @@ -60,9 +60,9 @@ func (ACLMappingVar) UseAs() sqltypes.Node { return ACLMappingVar{} } func (g ACLMappingVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) { // "left" will be a map of group names to actions in rego. - // { - // "all_users": ["read"] - // } + // { + // "all_users": ["read"] + // } left, err := sqltypes.RegoVarPath(g.StructPath, rego) if err != nil { return nil, false From 18638dacc4b499cf96c9a2fdf0795bf54b8a9847 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 30 Jul 2025 21:02:49 +0000 Subject: [PATCH 8/9] missing make gen --- docs/admin/security/audit-logs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 0a4b21a915315..0232c3d45a0c2 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -37,7 +37,7 @@ We track the following resources: | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| | WorkspaceBuild
start, stop | |
FieldTracked
ai_task_sidebar_app_idfalse
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| -| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| From 4e2458904a1fd74cf93c7cf7d4302b2891cb101f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 30 Jul 2025 21:15:46 +0000 Subject: [PATCH 9/9] fix more tests --- coderd/database/modelmethods.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index b49fa113d4b12..5347e8de37ebe 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -242,6 +242,8 @@ func (w Workspace) WorkspaceTable() WorkspaceTable { AutomaticUpdates: w.AutomaticUpdates, Favorite: w.Favorite, NextStartAt: w.NextStartAt, + GroupACL: w.GroupACL, + UserACL: w.UserACL, } }