mirror of
https://github.com/coder/coder.git
synced 2026-11-06 00:03:36 +08:00
Audit and connection log pages were timing out due to expensive COUNT(*) queries over large tables. This commit adds opt-in count capping: requests can return a `count_cap` field signaling that the count was truncated at a threshold, avoiding full table scans that caused page timeouts. Text-cast UUID comparisons in regosql-generated authorization queries also contributed to the slowdown by preventing index usage for connection and audit log queries. These now emit native UUID operators. Frontend changes handle the capped state in usePaginatedQuery and PaginationWidget, optionally displaying a capped count in the pagination UI (e.g. "Showing 2,076 to 2,100 of 2,000+ logs") Related to: https://linear.app/codercom/issue/PLAT-31/connectionaudit-log-performance-issue
166 lines
5.7 KiB
Go
166 lines
5.7 KiB
Go
package regosql
|
|
|
|
import "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
|
|
|
|
func resourceIDMatcher() sqltypes.VariableMatcher {
|
|
return sqltypes.StringVarMatcher("id :: text", []string{"input", "object", "id"})
|
|
}
|
|
|
|
func organizationOwnerMatcher() sqltypes.VariableMatcher {
|
|
return sqltypes.StringVarMatcher("organization_id :: text", []string{"input", "object", "org_owner"})
|
|
}
|
|
|
|
func userOwnerMatcher() sqltypes.VariableMatcher {
|
|
return sqltypes.StringVarMatcher("owner_id :: text", []string{"input", "object", "owner"})
|
|
}
|
|
|
|
func groupACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar {
|
|
return ACLMappingMatcher(m, "group_acl", []string{"input", "object", "acl_group_list"})
|
|
}
|
|
|
|
func userACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar {
|
|
return ACLMappingMatcher(m, "user_acl", []string{"input", "object", "acl_user_list"})
|
|
}
|
|
|
|
func TemplateConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
sqltypes.StringVarMatcher("t.organization_id :: text", []string{"input", "object", "org_owner"}),
|
|
// Templates have no user owner, only owner by an organization.
|
|
sqltypes.AlwaysFalse(userOwnerMatcher()),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
groupACLMatcher(matcher),
|
|
userACLMatcher(matcher),
|
|
)
|
|
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(
|
|
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
|
|
}
|
|
|
|
func AuditLogConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
sqltypes.UUIDVarMatcher("audit_logs.organization_id", []string{"input", "object", "org_owner"}),
|
|
// Audit logs have no user owner, only owner by an organization.
|
|
sqltypes.AlwaysFalse(userOwnerMatcher()),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
|
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
|
)
|
|
return matcher
|
|
}
|
|
|
|
func ConnectionLogConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
sqltypes.UUIDVarMatcher("connection_logs.organization_id", []string{"input", "object", "org_owner"}),
|
|
// Connection logs have no user owner, only owner by an organization.
|
|
sqltypes.AlwaysFalse(userOwnerMatcher()),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
|
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
|
)
|
|
return matcher
|
|
}
|
|
|
|
func AIBridgeInterceptionConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
// AI Bridge interceptions are not tied to any organization.
|
|
sqltypes.StringVarMatcher("''", []string{"input", "object", "org_owner"}),
|
|
sqltypes.StringVarMatcher("initiator_id :: text", []string{"input", "object", "owner"}),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
// No ACLs on the aibridge interception type
|
|
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
|
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
|
)
|
|
return matcher
|
|
}
|
|
|
|
func UserConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
// Users are never owned by an organization, so always return the empty string
|
|
// for the org owner.
|
|
sqltypes.StringVarMatcher("''", []string{"input", "object", "org_owner"}),
|
|
// Users are always owned by themselves.
|
|
sqltypes.StringVarMatcher("id :: text", []string{"input", "object", "owner"}),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
// No ACLs on the user type
|
|
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
|
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
|
)
|
|
return matcher
|
|
}
|
|
|
|
// NoACLConverter should be used when the target SQL table does not contain
|
|
// group or user ACL columns.
|
|
func NoACLConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
organizationOwnerMatcher(),
|
|
userOwnerMatcher(),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
|
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
|
)
|
|
|
|
return matcher
|
|
}
|
|
|
|
// ChatConverter should be used for the chats table, which has no
|
|
// organization_id, group_acl, or user_acl columns.
|
|
func ChatConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
// The chats table has no organization_id column. Map org_owner
|
|
// to a literal empty string so that:
|
|
// - User-level ownership checks (org_owner = '') activate correctly.
|
|
// - Org-scoped permissions never match (org_owner will never equal
|
|
// a real org UUID), which is intentional since chats are not
|
|
// org-scoped resources.
|
|
// Note: custom org roles that include "chat" permissions will
|
|
// silently have no effect because of this mapping.
|
|
sqltypes.StringVarMatcher("''", []string{"input", "object", "org_owner"}),
|
|
userOwnerMatcher(),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
|
|
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
|
|
)
|
|
|
|
return matcher
|
|
}
|
|
|
|
func DefaultVariableConverter() *sqltypes.VariableConverter {
|
|
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
|
resourceIDMatcher(),
|
|
organizationOwnerMatcher(),
|
|
userOwnerMatcher(),
|
|
)
|
|
matcher.RegisterMatcher(
|
|
groupACLMatcher(matcher),
|
|
userACLMatcher(matcher),
|
|
)
|
|
|
|
return matcher
|
|
}
|