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, } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy