diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7c3a1130db..4eba393b85 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12467,6 +12467,10 @@ const docTemplate = `{ "audit_log:*", "audit_log:create", "audit_log:read", + "boundary_usage:*", + "boundary_usage:delete", + "boundary_usage:read", + "boundary_usage:update", "coder:all", "coder:apikeys.manage_self", "coder:application_connect", @@ -12665,6 +12669,10 @@ const docTemplate = `{ "APIKeyScopeAuditLogAll", "APIKeyScopeAuditLogCreate", "APIKeyScopeAuditLogRead", + "APIKeyScopeBoundaryUsageAll", + "APIKeyScopeBoundaryUsageDelete", + "APIKeyScopeBoundaryUsageRead", + "APIKeyScopeBoundaryUsageUpdate", "APIKeyScopeCoderAll", "APIKeyScopeCoderApikeysManageSelf", "APIKeyScopeCoderApplicationConnect", @@ -17740,6 +17748,7 @@ const docTemplate = `{ "assign_org_role", "assign_role", "audit_log", + "boundary_usage", "connection_log", "crypto_key", "debug_info", @@ -17784,6 +17793,7 @@ const docTemplate = `{ "ResourceAssignOrgRole", "ResourceAssignRole", "ResourceAuditLog", + "ResourceBoundaryUsage", "ResourceConnectionLog", "ResourceCryptoKey", "ResourceDebugInfo", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1c4438bfaa..88d67ac8cb 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11105,6 +11105,10 @@ "audit_log:*", "audit_log:create", "audit_log:read", + "boundary_usage:*", + "boundary_usage:delete", + "boundary_usage:read", + "boundary_usage:update", "coder:all", "coder:apikeys.manage_self", "coder:application_connect", @@ -11303,6 +11307,10 @@ "APIKeyScopeAuditLogAll", "APIKeyScopeAuditLogCreate", "APIKeyScopeAuditLogRead", + "APIKeyScopeBoundaryUsageAll", + "APIKeyScopeBoundaryUsageDelete", + "APIKeyScopeBoundaryUsageRead", + "APIKeyScopeBoundaryUsageUpdate", "APIKeyScopeCoderAll", "APIKeyScopeCoderApikeysManageSelf", "APIKeyScopeCoderApplicationConnect", @@ -16182,6 +16190,7 @@ "assign_org_role", "assign_role", "audit_log", + "boundary_usage", "connection_log", "crypto_key", "debug_info", @@ -16226,6 +16235,7 @@ "ResourceAssignOrgRole", "ResourceAssignRole", "ResourceAuditLog", + "ResourceBoundaryUsage", "ResourceConnectionLog", "ResourceCryptoKey", "ResourceDebugInfo", diff --git a/coderd/boundaryusage/doc.go b/coderd/boundaryusage/doc.go new file mode 100644 index 0000000000..1089f5e5a3 --- /dev/null +++ b/coderd/boundaryusage/doc.go @@ -0,0 +1,79 @@ +// Package boundaryusage tracks workspace boundary usage for telemetry reporting. +// The design intent is to track trends and rough usage patterns. +// +// Each replica does in-memory usage tracking. Boundary usage is inferred at the +// control plane when workspace agents call the ReportBoundaryLogs RPC. Accumulated +// stats are periodically flushed to a database table keyed by replica ID. Telemetry +// aggregates are computed across all replicas when generating snapshots. +// +// Aggregate Precision: +// +// The aggregated stats represent approximate usage over roughly the telemetry +// snapshot interval, not a precise time window. This imprecision arises because: +// +// - Each replica flushes independently, so their data covers slightly different +// time ranges (varying by up to the flush interval) +// - Unflushed in-memory data at snapshot time rolls into the next period +// - The snapshot captures "data flushed since last reset" rather than "usage +// during exactly the last N minutes" +// +// We accept this imprecision to keep the architecture simple. Each replica +// operates independently and flushes to the database on their own schedule. +// This approach also minimizes database load. The table contains at most one +// row per replica, so flushes are just upserts, and resets only delete N +// rows. There's no accumulation of historical data to clean up. The only +// synchronization is a database lock that ensures exactly one replica reports +// telemetry per period. +// +// Known Shortcomings: +// +// - Unique workspace/user counts may be inflated when the same workspace or +// user connects through multiple replicas, as each replica tracks its own +// unique set +// - Ad-hoc boundary usage in a workspace may not be accounted for e.g. if +// the boundary command is invoked directly with the --log-proxy-socket-path +// flag set to something other than the Workspace agent server. +// +// Implementation: +// +// The Tracker maintains sets of unique workspace IDs and user IDs, plus request +// counters. When boundary logs are reported, Track() adds the IDs to the sets +// and increments request counters. +// +// FlushToDB() writes stats to the database, replacing all values with the current +// in-memory state. Stats accumulate in memory throughout the telemetry period. +// +// A new period is detected when the upsert results in an INSERT (meaning +// telemetry deleted the replica's row). At that point, all in-memory stats are +// reset so they only count usage within the new period. +// +// Below is a sequence diagram showing the flow of boundary usage tracking. +// +// ┌───────┐ ┌───────────────┐ ┌──────────┐ ┌────┐ ┌───────────┐ +// │ Agent │ │BoundaryLogsAPI│ │ Tracker │ │ DB │ │ Telemetry │ +// └───┬───┘ └───────┬───────┘ └────┬─────┘ └──┬─┘ └─────┬─────┘ +// │ │ │ │ │ +// │ ReportBoundaryLogs│ │ │ │ +// ├──────────────────►│ │ │ │ +// │ │ Track(...) │ │ │ +// │ ├────────────────►│ │ │ +// │ : │ │ │ │ +// │ : │ │ │ │ +// │ ReportBoundaryLogs│ │ │ │ +// ├──────────────────►│ │ │ │ +// │ │ Track(...) │ │ │ +// │ ├────────────────►│ │ │ +// │ │ │ │ │ +// │ │ │ FlushToDB │ │ +// │ │ ├────────────►│ │ +// │ │ │ : │ │ +// │ │ │ : │ │ +// │ │ │ FlushToDB │ │ +// │ │ ├────────────►│ │ +// │ │ │ │ │ +// │ │ │ │ Snapshot │ +// │ │ │ │ interval │ +// │ │ │ │◄───────────┤ +// │ │ │ │ Aggregate │ +// │ │ │ │ & Reset │ +package boundaryusage diff --git a/coderd/boundaryusage/tracker.go b/coderd/boundaryusage/tracker.go new file mode 100644 index 0000000000..1aebb0f53c --- /dev/null +++ b/coderd/boundaryusage/tracker.go @@ -0,0 +1,40 @@ +package boundaryusage + +import ( + "context" + "sync" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" +) + +// Tracker tracks boundary usage for telemetry reporting. +// +// All stats accumulate in memory throughout a telemetry period and are only +// reset when a new period begins. +type Tracker struct { + mu sync.Mutex //nolint:unused // Will be used when implemented. + workspaces map[uuid.UUID]struct{} //nolint:unused // Will be used when implemented. + users map[uuid.UUID]struct{} //nolint:unused // Will be used when implemented. + allowedRequests int64 //nolint:unused // Will be used when implemented. + deniedRequests int64 //nolint:unused // Will be used when implemented. +} + +// NewTracker creates a new boundary usage tracker. +func NewTracker() (*Tracker, error) { + return nil, xerrors.New("not implemented") +} + +// Track records boundary usage for a workspace. +func (*Tracker) Track(_, _ uuid.UUID, _, _ int64) error { + return xerrors.New("not implemented") +} + +// FlushToDB writes the accumulated stats to the database. All values are +// replaced in the database (they represent the current in-memory state). If the +// database row was deleted (new telemetry period), all in-memory stats are reset. +func (*Tracker) FlushToDB(_ context.Context, _ database.Store, _ uuid.UUID) error { + return xerrors.New("not implemented") +} diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 5de1586c98..812cd51e93 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -649,6 +649,25 @@ var ( }), Scope: rbac.ScopeAll, }.WithCachedASTValue() + + // Used by the boundary usage tracker to record telemetry statistics. + subjectBoundaryUsageTracker = rbac.Subject{ + Type: rbac.SubjectTypeBoundaryUsageTracker, + FriendlyName: "Boundary Usage Tracker", + ID: uuid.Nil.String(), + Roles: rbac.Roles([]rbac.Role{ + { + Identifier: rbac.RoleIdentifier{Name: "boundary-usage-tracker"}, + DisplayName: "Boundary Usage Tracker", + Site: rbac.Permissions(map[string][]policy.Action{ + rbac.ResourceBoundaryUsage.Type: rbac.ResourceBoundaryUsage.AvailableActions(), + }), + User: []rbac.Permission{}, + ByOrgID: map[string]rbac.OrgPermissions{}, + }, + }), + Scope: rbac.ScopeAll, + }.WithCachedASTValue() ) // AsProvisionerd returns a context with an actor that has permissions required @@ -749,6 +768,12 @@ func AsDBPurge(ctx context.Context) context.Context { return As(ctx, subjectDBPurge) } +// AsBoundaryUsageTracker returns a context with an actor that has permissions +// required for the boundary usage tracker to record telemetry statistics. +func AsBoundaryUsageTracker(ctx context.Context) context.Context { + return As(ctx, subjectBoundaryUsageTracker) +} + var AsRemoveActor = rbac.Subject{ ID: "remove-actor", } @@ -1678,6 +1703,13 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID) } +func (q *querier) DeleteBoundaryUsageStatsByReplicaID(ctx context.Context, replicaID uuid.UUID) error { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceBoundaryUsage); err != nil { + return err + } + return q.db.DeleteBoundaryUsageStatsByReplicaID(ctx, replicaID) +} + func (q *querier) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err @@ -2239,6 +2271,13 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI return q.db.GetAuthorizationUserRoles(ctx, userID) } +func (q *querier) GetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (database.GetBoundaryUsageSummaryRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryUsage); err != nil { + return database.GetBoundaryUsageSummaryRow{}, err + } + return q.db.GetBoundaryUsageSummary(ctx, maxStalenessMs) +} + func (q *querier) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) { // Just like with the audit logs query, shortcut if the user is an owner. err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog) @@ -4852,6 +4891,13 @@ func (q *querier) RemoveUserFromGroups(ctx context.Context, arg database.RemoveU return q.db.RemoveUserFromGroups(ctx, arg) } +func (q *querier) ResetBoundaryUsageStats(ctx context.Context) error { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceBoundaryUsage); err != nil { + return err + } + return q.db.ResetBoundaryUsageStats(ctx) +} + func (q *querier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err @@ -5943,6 +5989,13 @@ func (q *querier) UpsertApplicationName(ctx context.Context, value string) error return q.db.UpsertApplicationName(ctx, value) } +func (q *querier) UpsertBoundaryUsageStats(ctx context.Context, arg database.UpsertBoundaryUsageStatsParams) (bool, error) { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceBoundaryUsage); err != nil { + return false, err + } + return q.db.UpsertBoundaryUsageStats(ctx, arg) +} + func (q *querier) UpsertConnectionLog(ctx context.Context, arg database.UpsertConnectionLogParams) (database.ConnectionLog, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceConnectionLog); err != nil { return database.ConnectionLog{}, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0646de4214..7764f69b7d 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -278,6 +278,11 @@ func (s *MethodTestSuite) TestAPIKey() { dbm.EXPECT().DeleteApplicationConnectAPIKeysByUserID(gomock.Any(), a.UserID).Return(nil).AnyTimes() check.Args(a.UserID).Asserts(rbac.ResourceApiKey.WithOwner(a.UserID.String()), policy.ActionDelete).Returns() })) + s.Run("DeleteBoundaryUsageStatsByReplicaID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + replicaID := uuid.New() + dbm.EXPECT().DeleteBoundaryUsageStatsByReplicaID(gomock.Any(), replicaID).Return(nil).AnyTimes() + check.Args(replicaID).Asserts(rbac.ResourceBoundaryUsage, policy.ActionDelete) + })) s.Run("DeleteExternalAuthLink", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { a := testutil.Fake(s.T(), faker, database.ExternalAuthLink{}) dbm.EXPECT().GetExternalAuthLink(gomock.Any(), database.GetExternalAuthLinkParams{ProviderID: a.ProviderID, UserID: a.UserID}).Return(a, nil).AnyTimes() @@ -528,6 +533,10 @@ func (s *MethodTestSuite) TestGroup() { dbm.EXPECT().RemoveUserFromGroups(gomock.Any(), arg).Return(slice.New(g1.ID, g2.ID), nil).AnyTimes() check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(slice.New(g1.ID, g2.ID)) })) + s.Run("ResetBoundaryUsageStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().ResetBoundaryUsageStats(gomock.Any()).Return(nil).AnyTimes() + check.Args().Asserts(rbac.ResourceBoundaryUsage, policy.ActionDelete) + })) s.Run("UpdateGroupByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { g := testutil.Fake(s.T(), faker, database.Group{}) @@ -2972,6 +2981,10 @@ func (s *MethodTestSuite) TestSystemFunctions() { dbm.EXPECT().GetAuthorizationUserRoles(gomock.Any(), u.ID).Return(database.GetAuthorizationUserRolesRow{}, nil).AnyTimes() check.Args(u.ID).Asserts(rbac.ResourceSystem, policy.ActionRead) })) + s.Run("GetBoundaryUsageSummary", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + dbm.EXPECT().GetBoundaryUsageSummary(gomock.Any(), int64(1000)).Return(database.GetBoundaryUsageSummaryRow{}, nil).AnyTimes() + check.Args(int64(1000)).Asserts(rbac.ResourceBoundaryUsage, policy.ActionRead) + })) s.Run("GetDERPMeshKey", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().GetDERPMeshKey(gomock.Any()).Return("testing", nil).AnyTimes() check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead) @@ -3342,6 +3355,11 @@ func (s *MethodTestSuite) TestSystemFunctions() { dbm.EXPECT().UpsertApplicationName(gomock.Any(), "").Return(nil).AnyTimes() check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate) })) + s.Run("UpsertBoundaryUsageStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { + arg := database.UpsertBoundaryUsageStatsParams{ReplicaID: uuid.New()} + dbm.EXPECT().UpsertBoundaryUsageStats(gomock.Any(), arg).Return(false, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceBoundaryUsage, policy.ActionUpdate) + })) s.Run("GetHealthSettings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().GetHealthSettings(gomock.Any()).Return("{}", nil).AnyTimes() check.Args().Asserts() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 97469853fc..b7ecd59546 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -335,6 +335,14 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C return r0 } +func (m queryMetricsStore) DeleteBoundaryUsageStatsByReplicaID(ctx context.Context, replicaID uuid.UUID) error { + start := time.Now() + r0 := m.s.DeleteBoundaryUsageStatsByReplicaID(ctx, replicaID) + m.queryLatencies.WithLabelValues("DeleteBoundaryUsageStatsByReplicaID").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteBoundaryUsageStatsByReplicaID").Inc() + return r0 +} + func (m queryMetricsStore) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) { start := time.Now() r0, r1 := m.s.DeleteCryptoKey(ctx, arg) @@ -894,6 +902,14 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID return r0, r1 } +func (m queryMetricsStore) GetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (database.GetBoundaryUsageSummaryRow, error) { + start := time.Now() + r0, r1 := m.s.GetBoundaryUsageSummary(ctx, maxStalenessMs) + m.queryLatencies.WithLabelValues("GetBoundaryUsageSummary").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetBoundaryUsageSummary").Inc() + return r0, r1 +} + func (m queryMetricsStore) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) { start := time.Now() r0, r1 := m.s.GetConnectionLogsOffset(ctx, arg) @@ -3318,6 +3334,14 @@ func (m queryMetricsStore) RemoveUserFromGroups(ctx context.Context, arg databas return r0, r1 } +func (m queryMetricsStore) ResetBoundaryUsageStats(ctx context.Context) error { + start := time.Now() + r0 := m.s.ResetBoundaryUsageStats(ctx) + m.queryLatencies.WithLabelValues("ResetBoundaryUsageStats").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ResetBoundaryUsageStats").Inc() + return r0 +} + func (m queryMetricsStore) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error { start := time.Now() r0 := m.s.RevokeDBCryptKey(ctx, activeKeyDigest) @@ -4069,6 +4093,14 @@ func (m queryMetricsStore) UpsertApplicationName(ctx context.Context, value stri return r0 } +func (m queryMetricsStore) UpsertBoundaryUsageStats(ctx context.Context, arg database.UpsertBoundaryUsageStatsParams) (bool, error) { + start := time.Now() + r0, r1 := m.s.UpsertBoundaryUsageStats(ctx, arg) + m.queryLatencies.WithLabelValues("UpsertBoundaryUsageStats").Observe(time.Since(start).Seconds()) + m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertBoundaryUsageStats").Inc() + return r0, r1 +} + func (m queryMetricsStore) UpsertConnectionLog(ctx context.Context, arg database.UpsertConnectionLogParams) (database.ConnectionLog, error) { start := time.Now() r0, r1 := m.s.UpsertConnectionLog(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 7e329cfe5f..bf560b513a 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -511,6 +511,20 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID) } +// DeleteBoundaryUsageStatsByReplicaID mocks base method. +func (m *MockStore) DeleteBoundaryUsageStatsByReplicaID(ctx context.Context, replicaID uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteBoundaryUsageStatsByReplicaID", ctx, replicaID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteBoundaryUsageStatsByReplicaID indicates an expected call of DeleteBoundaryUsageStatsByReplicaID. +func (mr *MockStoreMockRecorder) DeleteBoundaryUsageStatsByReplicaID(ctx, replicaID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoundaryUsageStatsByReplicaID", reflect.TypeOf((*MockStore)(nil).DeleteBoundaryUsageStatsByReplicaID), ctx, replicaID) +} + // DeleteCryptoKey mocks base method. func (m *MockStore) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) { m.ctrl.T.Helper() @@ -1634,6 +1648,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared) } +// GetBoundaryUsageSummary mocks base method. +func (m *MockStore) GetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (database.GetBoundaryUsageSummaryRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBoundaryUsageSummary", ctx, maxStalenessMs) + ret0, _ := ret[0].(database.GetBoundaryUsageSummaryRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBoundaryUsageSummary indicates an expected call of GetBoundaryUsageSummary. +func (mr *MockStoreMockRecorder) GetBoundaryUsageSummary(ctx, maxStalenessMs any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoundaryUsageSummary", reflect.TypeOf((*MockStore)(nil).GetBoundaryUsageSummary), ctx, maxStalenessMs) +} + // GetConnectionLogsOffset mocks base method. func (m *MockStore) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) { m.ctrl.T.Helper() @@ -6249,6 +6278,20 @@ func (mr *MockStoreMockRecorder) RemoveUserFromGroups(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserFromGroups", reflect.TypeOf((*MockStore)(nil).RemoveUserFromGroups), ctx, arg) } +// ResetBoundaryUsageStats mocks base method. +func (m *MockStore) ResetBoundaryUsageStats(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResetBoundaryUsageStats", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// ResetBoundaryUsageStats indicates an expected call of ResetBoundaryUsageStats. +func (mr *MockStoreMockRecorder) ResetBoundaryUsageStats(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetBoundaryUsageStats", reflect.TypeOf((*MockStore)(nil).ResetBoundaryUsageStats), ctx) +} + // RevokeDBCryptKey mocks base method. func (m *MockStore) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error { m.ctrl.T.Helper() @@ -7603,6 +7646,21 @@ func (mr *MockStoreMockRecorder) UpsertApplicationName(ctx, value any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertApplicationName", reflect.TypeOf((*MockStore)(nil).UpsertApplicationName), ctx, value) } +// UpsertBoundaryUsageStats mocks base method. +func (m *MockStore) UpsertBoundaryUsageStats(ctx context.Context, arg database.UpsertBoundaryUsageStatsParams) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertBoundaryUsageStats", ctx, arg) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertBoundaryUsageStats indicates an expected call of UpsertBoundaryUsageStats. +func (mr *MockStoreMockRecorder) UpsertBoundaryUsageStats(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertBoundaryUsageStats", reflect.TypeOf((*MockStore)(nil).UpsertBoundaryUsageStats), ctx, arg) +} + // UpsertConnectionLog mocks base method. func (m *MockStore) UpsertConnectionLog(ctx context.Context, arg database.UpsertConnectionLogParams) (database.ConnectionLog, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 0a1c494f35..6bfcc24dfa 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -204,7 +204,11 @@ CREATE TYPE api_key_scope AS ENUM ( 'task:delete', 'task:*', 'workspace:share', - 'workspace_dormant:share' + 'workspace_dormant:share', + 'boundary_usage:*', + 'boundary_usage:delete', + 'boundary_usage:read', + 'boundary_usage:update' ); CREATE TYPE app_sharing_level AS ENUM ( @@ -1111,6 +1115,32 @@ CREATE TABLE audit_logs ( resource_icon text NOT NULL ); +CREATE TABLE boundary_usage_stats ( + replica_id uuid NOT NULL, + unique_workspaces_count bigint DEFAULT 0 NOT NULL, + unique_users_count bigint DEFAULT 0 NOT NULL, + allowed_requests bigint DEFAULT 0 NOT NULL, + denied_requests bigint DEFAULT 0 NOT NULL, + window_start timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + +COMMENT ON TABLE boundary_usage_stats IS 'Per-replica boundary usage statistics for telemetry aggregation.'; + +COMMENT ON COLUMN boundary_usage_stats.replica_id IS 'The unique identifier of the replica reporting stats.'; + +COMMENT ON COLUMN boundary_usage_stats.unique_workspaces_count IS 'Count of unique workspaces that used boundary on this replica.'; + +COMMENT ON COLUMN boundary_usage_stats.unique_users_count IS 'Count of unique users that used boundary on this replica.'; + +COMMENT ON COLUMN boundary_usage_stats.allowed_requests IS 'Total allowed requests through boundary on this replica.'; + +COMMENT ON COLUMN boundary_usage_stats.denied_requests IS 'Total denied requests through boundary on this replica.'; + +COMMENT ON COLUMN boundary_usage_stats.window_start IS 'Start of the time window for these stats, set on first flush after reset.'; + +COMMENT ON COLUMN boundary_usage_stats.updated_at IS 'Timestamp of the last update to this row.'; + CREATE TABLE connection_logs ( id uuid NOT NULL, connect_time timestamp with time zone NOT NULL, @@ -2002,7 +2032,7 @@ CREATE TABLE telemetry_items ( CREATE TABLE telemetry_locks ( event_type text NOT NULL, period_ending_at timestamp with time zone NOT NULL, - CONSTRAINT telemetry_lock_event_type_constraint CHECK ((event_type = 'aibridge_interceptions_summary'::text)) + CONSTRAINT telemetry_lock_event_type_constraint CHECK ((event_type = ANY (ARRAY['aibridge_interceptions_summary'::text, 'boundary_usage_summary'::text]))) ); COMMENT ON TABLE telemetry_locks IS 'Telemetry lock tracking table for deduplication of heartbeat events across replicas.'; @@ -2941,6 +2971,9 @@ ALTER TABLE ONLY api_keys ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); +ALTER TABLE ONLY boundary_usage_stats + ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id); + ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_pkey PRIMARY KEY (id); diff --git a/coderd/database/migrations/000411_boundary_usage_stats.down.sql b/coderd/database/migrations/000411_boundary_usage_stats.down.sql new file mode 100644 index 0000000000..83d637efdb --- /dev/null +++ b/coderd/database/migrations/000411_boundary_usage_stats.down.sql @@ -0,0 +1,8 @@ +-- Restore the original telemetry_locks event_type constraint. +ALTER TABLE telemetry_locks DROP CONSTRAINT telemetry_lock_event_type_constraint; +ALTER TABLE telemetry_locks ADD CONSTRAINT telemetry_lock_event_type_constraint + CHECK (event_type IN ('aibridge_interceptions_summary')); + +DROP TABLE boundary_usage_stats; + +-- No-op for boundary_usage scopes: keep enum values to avoid dependency churn. diff --git a/coderd/database/migrations/000411_boundary_usage_stats.up.sql b/coderd/database/migrations/000411_boundary_usage_stats.up.sql new file mode 100644 index 0000000000..26fce4f9cd --- /dev/null +++ b/coderd/database/migrations/000411_boundary_usage_stats.up.sql @@ -0,0 +1,29 @@ +CREATE TABLE boundary_usage_stats ( + replica_id UUID PRIMARY KEY, + unique_workspaces_count BIGINT NOT NULL DEFAULT 0, + unique_users_count BIGINT NOT NULL DEFAULT 0, + allowed_requests BIGINT NOT NULL DEFAULT 0, + denied_requests BIGINT NOT NULL DEFAULT 0, + window_start TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE boundary_usage_stats IS 'Per-replica boundary usage statistics for telemetry aggregation.'; +COMMENT ON COLUMN boundary_usage_stats.replica_id IS 'The unique identifier of the replica reporting stats.'; +COMMENT ON COLUMN boundary_usage_stats.unique_workspaces_count IS 'Count of unique workspaces that used boundary on this replica.'; +COMMENT ON COLUMN boundary_usage_stats.unique_users_count IS 'Count of unique users that used boundary on this replica.'; +COMMENT ON COLUMN boundary_usage_stats.allowed_requests IS 'Total allowed requests through boundary on this replica.'; +COMMENT ON COLUMN boundary_usage_stats.denied_requests IS 'Total denied requests through boundary on this replica.'; +COMMENT ON COLUMN boundary_usage_stats.window_start IS 'Start of the time window for these stats, set on first flush after reset.'; +COMMENT ON COLUMN boundary_usage_stats.updated_at IS 'Timestamp of the last update to this row.'; + +-- Add boundary_usage_summary to the telemetry_locks event_type constraint. +ALTER TABLE telemetry_locks DROP CONSTRAINT telemetry_lock_event_type_constraint; +ALTER TABLE telemetry_locks ADD CONSTRAINT telemetry_lock_event_type_constraint + CHECK (event_type IN ('aibridge_interceptions_summary', 'boundary_usage_summary')); + +-- Add boundary_usage scopes for RBAC. +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'boundary_usage:*'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'boundary_usage:delete'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'boundary_usage:read'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'boundary_usage:update'; diff --git a/coderd/database/migrations/testdata/fixtures/000411_boundary_usage_stats.up.sql b/coderd/database/migrations/testdata/fixtures/000411_boundary_usage_stats.up.sql new file mode 100644 index 0000000000..790e12691d --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000411_boundary_usage_stats.up.sql @@ -0,0 +1,2 @@ +INSERT INTO boundary_usage_stats (replica_id, unique_workspaces_count, unique_users_count, allowed_requests, denied_requests, window_start, updated_at) +VALUES ('00000000-0000-0000-0000-000000000001', 10, 5, 100, 20, NOW(), NOW()); diff --git a/coderd/database/models.go b/coderd/database/models.go index 6f94bbea49..7a9e8962a7 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -213,6 +213,10 @@ const ( ApiKeyScopeTask APIKeyScope = "task:*" ApiKeyScopeWorkspaceShare APIKeyScope = "workspace:share" ApiKeyScopeWorkspaceDormantShare APIKeyScope = "workspace_dormant:share" + ApiKeyScopeBoundaryUsage APIKeyScope = "boundary_usage:*" + ApiKeyScopeBoundaryUsageDelete APIKeyScope = "boundary_usage:delete" + ApiKeyScopeBoundaryUsageRead APIKeyScope = "boundary_usage:read" + ApiKeyScopeBoundaryUsageUpdate APIKeyScope = "boundary_usage:update" ) func (e *APIKeyScope) Scan(src interface{}) error { @@ -445,7 +449,11 @@ func (e APIKeyScope) Valid() bool { ApiKeyScopeTaskDelete, ApiKeyScopeTask, ApiKeyScopeWorkspaceShare, - ApiKeyScopeWorkspaceDormantShare: + ApiKeyScopeWorkspaceDormantShare, + ApiKeyScopeBoundaryUsage, + ApiKeyScopeBoundaryUsageDelete, + ApiKeyScopeBoundaryUsageRead, + ApiKeyScopeBoundaryUsageUpdate: return true } return false @@ -647,6 +655,10 @@ func AllAPIKeyScopeValues() []APIKeyScope { ApiKeyScopeTask, ApiKeyScopeWorkspaceShare, ApiKeyScopeWorkspaceDormantShare, + ApiKeyScopeBoundaryUsage, + ApiKeyScopeBoundaryUsageDelete, + ApiKeyScopeBoundaryUsageRead, + ApiKeyScopeBoundaryUsageUpdate, } } @@ -3702,6 +3714,24 @@ type AuditLog struct { ResourceIcon string `db:"resource_icon" json:"resource_icon"` } +// Per-replica boundary usage statistics for telemetry aggregation. +type BoundaryUsageStat struct { + // The unique identifier of the replica reporting stats. + ReplicaID uuid.UUID `db:"replica_id" json:"replica_id"` + // Count of unique workspaces that used boundary on this replica. + UniqueWorkspacesCount int64 `db:"unique_workspaces_count" json:"unique_workspaces_count"` + // Count of unique users that used boundary on this replica. + UniqueUsersCount int64 `db:"unique_users_count" json:"unique_users_count"` + // Total allowed requests through boundary on this replica. + AllowedRequests int64 `db:"allowed_requests" json:"allowed_requests"` + // Total denied requests through boundary on this replica. + DeniedRequests int64 `db:"denied_requests" json:"denied_requests"` + // Start of the time window for these stats, set on first flush after reset. + WindowStart time.Time `db:"window_start" json:"window_start"` + // Timestamp of the last update to this row. + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + type ConnectionLog struct { ID uuid.UUID `db:"id" json:"id"` ConnectTime time.Time `db:"connect_time" json:"connect_time"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index d4c3e04a4c..464ac606c9 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -88,6 +88,8 @@ type sqlcQuerier interface { // be recreated. DeleteAllWebpushSubscriptions(ctx context.Context) error DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error + // Deletes boundary usage statistics for a specific replica. + DeleteBoundaryUsageStatsByReplicaID(ctx context.Context, replicaID uuid.UUID) error DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error) DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error) @@ -194,6 +196,10 @@ type sqlcQuerier interface { // This function returns roles for authorization purposes. Implied member roles // are included. GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) + // Aggregates boundary usage statistics across all replicas. Filters to only + // include data where window_start is within the given interval to exclude + // stale data. + GetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (GetBoundaryUsageSummaryRow, error) GetConnectionLogsOffset(ctx context.Context, arg GetConnectionLogsOffsetParams) ([]GetConnectionLogsOffsetRow, error) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error) @@ -646,6 +652,9 @@ type sqlcQuerier interface { RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error) + // Deletes all boundary usage statistics. Called after telemetry reports the + // aggregated stats. Each replica will insert a fresh row on its next flush. + ResetBoundaryUsageStats(ctx context.Context) error RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error // Note that this selects from the CTE, not the original table. The CTE is named // the same as the original table to trick sqlc into reusing the existing struct @@ -753,6 +762,10 @@ type sqlcQuerier interface { UpsertAnnouncementBanners(ctx context.Context, value string) error UpsertAppSecurityKey(ctx context.Context, value string) error UpsertApplicationName(ctx context.Context, value string) error + // Upserts boundary usage statistics for a replica. All values are replaced with + // the current in-memory state. Returns true if this was an insert (new period), + // false if update. + UpsertBoundaryUsageStats(ctx context.Context, arg UpsertBoundaryUsageStatsParams) (bool, error) UpsertConnectionLog(ctx context.Context, arg UpsertConnectionLogParams) (ConnectionLog, error) UpsertCoordinatorResumeTokenSigningKey(ctx context.Context, value string) error // The default proxy is implied and not actually stored in the database. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8a1188a2e0..e4552a2ff1 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1980,6 +1980,109 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam return i, err } +const deleteBoundaryUsageStatsByReplicaID = `-- name: DeleteBoundaryUsageStatsByReplicaID :exec +DELETE FROM boundary_usage_stats WHERE replica_id = $1 +` + +// Deletes boundary usage statistics for a specific replica. +func (q *sqlQuerier) DeleteBoundaryUsageStatsByReplicaID(ctx context.Context, replicaID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteBoundaryUsageStatsByReplicaID, replicaID) + return err +} + +const getBoundaryUsageSummary = `-- name: GetBoundaryUsageSummary :one +SELECT + COALESCE(SUM(unique_workspaces_count), 0)::bigint AS unique_workspaces, + COALESCE(SUM(unique_users_count), 0)::bigint AS unique_users, + COALESCE(SUM(allowed_requests), 0)::bigint AS allowed_requests, + COALESCE(SUM(denied_requests), 0)::bigint AS denied_requests +FROM boundary_usage_stats +WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval +` + +type GetBoundaryUsageSummaryRow struct { + UniqueWorkspaces int64 `db:"unique_workspaces" json:"unique_workspaces"` + UniqueUsers int64 `db:"unique_users" json:"unique_users"` + AllowedRequests int64 `db:"allowed_requests" json:"allowed_requests"` + DeniedRequests int64 `db:"denied_requests" json:"denied_requests"` +} + +// Aggregates boundary usage statistics across all replicas. Filters to only +// include data where window_start is within the given interval to exclude +// stale data. +func (q *sqlQuerier) GetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (GetBoundaryUsageSummaryRow, error) { + row := q.db.QueryRowContext(ctx, getBoundaryUsageSummary, maxStalenessMs) + var i GetBoundaryUsageSummaryRow + err := row.Scan( + &i.UniqueWorkspaces, + &i.UniqueUsers, + &i.AllowedRequests, + &i.DeniedRequests, + ) + return i, err +} + +const resetBoundaryUsageStats = `-- name: ResetBoundaryUsageStats :exec +DELETE FROM boundary_usage_stats +` + +// Deletes all boundary usage statistics. Called after telemetry reports the +// aggregated stats. Each replica will insert a fresh row on its next flush. +func (q *sqlQuerier) ResetBoundaryUsageStats(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, resetBoundaryUsageStats) + return err +} + +const upsertBoundaryUsageStats = `-- name: UpsertBoundaryUsageStats :one +INSERT INTO boundary_usage_stats ( + replica_id, + unique_workspaces_count, + unique_users_count, + allowed_requests, + denied_requests, + window_start, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + NOW(), + NOW() +) ON CONFLICT (replica_id) DO UPDATE SET + unique_workspaces_count = EXCLUDED.unique_workspaces_count, + unique_users_count = EXCLUDED.unique_users_count, + allowed_requests = EXCLUDED.allowed_requests, + denied_requests = EXCLUDED.denied_requests, + updated_at = NOW() +RETURNING (xmax = 0) AS new_period +` + +type UpsertBoundaryUsageStatsParams struct { + ReplicaID uuid.UUID `db:"replica_id" json:"replica_id"` + UniqueWorkspacesCount int64 `db:"unique_workspaces_count" json:"unique_workspaces_count"` + UniqueUsersCount int64 `db:"unique_users_count" json:"unique_users_count"` + AllowedRequests int64 `db:"allowed_requests" json:"allowed_requests"` + DeniedRequests int64 `db:"denied_requests" json:"denied_requests"` +} + +// Upserts boundary usage statistics for a replica. All values are replaced with +// the current in-memory state. Returns true if this was an insert (new period), +// false if update. +func (q *sqlQuerier) UpsertBoundaryUsageStats(ctx context.Context, arg UpsertBoundaryUsageStatsParams) (bool, error) { + row := q.db.QueryRowContext(ctx, upsertBoundaryUsageStats, + arg.ReplicaID, + arg.UniqueWorkspacesCount, + arg.UniqueUsersCount, + arg.AllowedRequests, + arg.DeniedRequests, + ) + var new_period bool + err := row.Scan(&new_period) + return new_period, err +} + const countConnectionLogs = `-- name: CountConnectionLogs :one SELECT COUNT(*) AS count diff --git a/coderd/database/queries/boundaryusagestats.sql b/coderd/database/queries/boundaryusagestats.sql new file mode 100644 index 0000000000..7803bcae7a --- /dev/null +++ b/coderd/database/queries/boundaryusagestats.sql @@ -0,0 +1,48 @@ +-- name: UpsertBoundaryUsageStats :one +-- Upserts boundary usage statistics for a replica. All values are replaced with +-- the current in-memory state. Returns true if this was an insert (new period), +-- false if update. +INSERT INTO boundary_usage_stats ( + replica_id, + unique_workspaces_count, + unique_users_count, + allowed_requests, + denied_requests, + window_start, + updated_at +) VALUES ( + @replica_id, + @unique_workspaces_count, + @unique_users_count, + @allowed_requests, + @denied_requests, + NOW(), + NOW() +) ON CONFLICT (replica_id) DO UPDATE SET + unique_workspaces_count = EXCLUDED.unique_workspaces_count, + unique_users_count = EXCLUDED.unique_users_count, + allowed_requests = EXCLUDED.allowed_requests, + denied_requests = EXCLUDED.denied_requests, + updated_at = NOW() +RETURNING (xmax = 0) AS new_period; + +-- name: GetBoundaryUsageSummary :one +-- Aggregates boundary usage statistics across all replicas. Filters to only +-- include data where window_start is within the given interval to exclude +-- stale data. +SELECT + COALESCE(SUM(unique_workspaces_count), 0)::bigint AS unique_workspaces, + COALESCE(SUM(unique_users_count), 0)::bigint AS unique_users, + COALESCE(SUM(allowed_requests), 0)::bigint AS allowed_requests, + COALESCE(SUM(denied_requests), 0)::bigint AS denied_requests +FROM boundary_usage_stats +WHERE window_start >= NOW() - (@max_staleness_ms::bigint || ' ms')::interval; + +-- name: ResetBoundaryUsageStats :exec +-- Deletes all boundary usage statistics. Called after telemetry reports the +-- aggregated stats. Each replica will insert a fresh row on its next flush. +DELETE FROM boundary_usage_stats; + +-- name: DeleteBoundaryUsageStatsByReplicaID :exec +-- Deletes boundary usage statistics for a specific replica. +DELETE FROM boundary_usage_stats WHERE replica_id = @replica_id; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index a7d15bf3c3..40357f38c9 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -13,6 +13,7 @@ const ( UniqueAibridgeUserPromptsPkey UniqueConstraint = "aibridge_user_prompts_pkey" // ALTER TABLE ONLY aibridge_user_prompts ADD CONSTRAINT aibridge_user_prompts_pkey PRIMARY KEY (id); UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id); UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); + UniqueBoundaryUsageStatsPkey UniqueConstraint = "boundary_usage_stats_pkey" // ALTER TABLE ONLY boundary_usage_stats ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id); UniqueConnectionLogsPkey UniqueConstraint = "connection_logs_pkey" // ALTER TABLE ONLY connection_logs ADD CONSTRAINT connection_logs_pkey PRIMARY KEY (id); UniqueCryptoKeysPkey UniqueConstraint = "crypto_keys_pkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence); UniqueCustomRolesUniqueKey UniqueConstraint = "custom_roles_unique_key" // ALTER TABLE ONLY custom_roles ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id); diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 7a8ad7e813..8e39734c10 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -80,6 +80,7 @@ const ( SubjectTypeUsagePublisher SubjectType = "usage_publisher" SubjectAibridged SubjectType = "aibridged" SubjectTypeDBPurge SubjectType = "dbpurge" + SubjectTypeBoundaryUsageTracker SubjectType = "boundary_usage_tracker" ) const ( diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index c71b74d496..438ab2b069 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -63,6 +63,15 @@ var ( Type: "audit_log", } + // ResourceBoundaryUsage + // Valid Actions + // - "ActionDelete" :: delete boundary usage statistics + // - "ActionRead" :: read boundary usage statistics + // - "ActionUpdate" :: upsert boundary usage statistics + ResourceBoundaryUsage = Object{ + Type: "boundary_usage", + } + // ResourceConnectionLog // Valid Actions // - "ActionRead" :: read connection logs @@ -417,6 +426,7 @@ func AllResources() []Objecter { ResourceAssignOrgRole, ResourceAssignRole, ResourceAuditLog, + ResourceBoundaryUsage, ResourceConnectionLog, ResourceCryptoKey, ResourceDebugInfo, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 8c4e2abaaa..97b5bbd6e4 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -380,4 +380,11 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionCreate: "create aibridge interceptions & related records", }, }, + "boundary_usage": { + Actions: map[Action]ActionDefinition{ + ActionRead: "read boundary usage statistics", + ActionUpdate: "upsert boundary usage statistics", + ActionDelete: "delete boundary usage statistics", + }, + }, } diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index c0094c7ecd..0c7508c64d 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -286,7 +286,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // Workspace dormancy and workspace are omitted. // Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec. // Owners cannot access other users' secrets. - allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUsageEvent), + allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUsageEvent, ResourceBoundaryUsage), // This adds back in the Workspace permissions. Permissions(map[string][]policy.Action{ ResourceWorkspace.Type: ownerWorkspaceActions, @@ -309,7 +309,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceOauth2App.Type: {policy.ActionRead}, ResourceWorkspaceProxy.Type: {policy.ActionRead}, }), - User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember), + User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. @@ -433,7 +433,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ByOrgID: map[string]OrgPermissions{ // Org admins should not have workspace exec perms. organizationID.String(): { - Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret), Permissions(map[string][]policy.Action{ + Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{ ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent}, ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), // PrebuiltWorkspaces are a subset of Workspaces. diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index b2402a318d..cad9d548fe 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -1003,6 +1003,14 @@ func TestRolePermissions(t *testing.T) { }, }, }, + { + Name: "BoundaryUsage", + Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + Resource: rbac.ResourceBoundaryUsage, + AuthorizeMap: map[bool][]hasAuthSubjects{ + false: {owner, setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, + }, + }, } // We expect every permission to be tested above. diff --git a/coderd/rbac/scopes_constants_gen.go b/coderd/rbac/scopes_constants_gen.go index 2bd058b5b1..b676b3f0d9 100644 --- a/coderd/rbac/scopes_constants_gen.go +++ b/coderd/rbac/scopes_constants_gen.go @@ -25,6 +25,9 @@ const ( ScopeAssignRoleUnassign ScopeName = "assign_role:unassign" ScopeAuditLogCreate ScopeName = "audit_log:create" ScopeAuditLogRead ScopeName = "audit_log:read" + ScopeBoundaryUsageDelete ScopeName = "boundary_usage:delete" + ScopeBoundaryUsageRead ScopeName = "boundary_usage:read" + ScopeBoundaryUsageUpdate ScopeName = "boundary_usage:update" ScopeConnectionLogRead ScopeName = "connection_log:read" ScopeConnectionLogUpdate ScopeName = "connection_log:update" ScopeCryptoKeyCreate ScopeName = "crypto_key:create" @@ -180,6 +183,9 @@ func (e ScopeName) Valid() bool { ScopeAssignRoleUnassign, ScopeAuditLogCreate, ScopeAuditLogRead, + ScopeBoundaryUsageDelete, + ScopeBoundaryUsageRead, + ScopeBoundaryUsageUpdate, ScopeConnectionLogRead, ScopeConnectionLogUpdate, ScopeCryptoKeyCreate, @@ -336,6 +342,9 @@ func AllScopeNameValues() []ScopeName { ScopeAssignRoleUnassign, ScopeAuditLogCreate, ScopeAuditLogRead, + ScopeBoundaryUsageDelete, + ScopeBoundaryUsageRead, + ScopeBoundaryUsageUpdate, ScopeConnectionLogRead, ScopeConnectionLogUpdate, ScopeCryptoKeyCreate, diff --git a/codersdk/apikey_scopes_gen.go b/codersdk/apikey_scopes_gen.go index f4bc90152d..45b8f01780 100644 --- a/codersdk/apikey_scopes_gen.go +++ b/codersdk/apikey_scopes_gen.go @@ -29,6 +29,10 @@ const ( APIKeyScopeAuditLogAll APIKeyScope = "audit_log:*" APIKeyScopeAuditLogCreate APIKeyScope = "audit_log:create" APIKeyScopeAuditLogRead APIKeyScope = "audit_log:read" + APIKeyScopeBoundaryUsageAll APIKeyScope = "boundary_usage:*" + APIKeyScopeBoundaryUsageDelete APIKeyScope = "boundary_usage:delete" + APIKeyScopeBoundaryUsageRead APIKeyScope = "boundary_usage:read" + APIKeyScopeBoundaryUsageUpdate APIKeyScope = "boundary_usage:update" APIKeyScopeCoderAll APIKeyScope = "coder:all" APIKeyScopeCoderApikeysManageSelf APIKeyScope = "coder:apikeys.manage_self" APIKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect" diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index b6f8e778ee..6b96928e23 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -10,6 +10,7 @@ const ( ResourceAssignOrgRole RBACResource = "assign_org_role" ResourceAssignRole RBACResource = "assign_role" ResourceAuditLog RBACResource = "audit_log" + ResourceBoundaryUsage RBACResource = "boundary_usage" ResourceConnectionLog RBACResource = "connection_log" ResourceCryptoKey RBACResource = "crypto_key" ResourceDebugInfo RBACResource = "debug_info" @@ -79,6 +80,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUnassign, ActionUpdate}, ResourceAssignRole: {ActionAssign, ActionRead, ActionUnassign}, ResourceAuditLog: {ActionCreate, ActionRead}, + ResourceBoundaryUsage: {ActionDelete, ActionRead, ActionUpdate}, ResourceConnectionLog: {ActionRead, ActionUpdate}, ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceDebugInfo: {ActionRead}, diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index aa091bb094..26669241ed 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -172,10 +172,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -305,10 +305,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -438,10 +438,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -533,10 +533,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -850,9 +850,9 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 94e8d79b82..8ae7819879 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -862,9 +862,9 @@ #### Enumerated Values -| Value(s) | -|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | +| Value(s) | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | ## codersdk.AddLicenseRequest @@ -7060,9 +7060,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit| #### Enumerated Values -| Value(s) | -|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| Value(s) | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | ## codersdk.RateLimitConfig diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index b034437cce..ffc7dd51a9 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -810,11 +810,11 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | -| `login_type` | `github`, `oidc`, `password`, `token` | -| `scope` | `all`, `application_connect` | +| Property | Value(s) | +|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` | +| `login_type` | `github`, `oidc`, `password`, `token` | +| `scope` | `all`, `application_connect` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index ff7501665b..8429003f3d 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -36,6 +36,11 @@ export const RBACResourceActions: Partial< create: "create new audit log entries", read: "read audit logs", }, + boundary_usage: { + delete: "delete boundary usage statistics", + read: "read boundary usage statistics", + update: "upsert boundary usage statistics", + }, connection_log: { read: "read connection logs", update: "upsert connection log entries", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b82983b0e2..10db5ed9c8 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -189,6 +189,10 @@ export type APIKeyScope = | "audit_log:*" | "audit_log:create" | "audit_log:read" + | "boundary_usage:*" + | "boundary_usage:delete" + | "boundary_usage:read" + | "boundary_usage:update" | "coder:all" | "coder:apikeys.manage_self" | "coder:application_connect" @@ -387,6 +391,10 @@ export const APIKeyScopes: APIKeyScope[] = [ "audit_log:*", "audit_log:create", "audit_log:read", + "boundary_usage:*", + "boundary_usage:delete", + "boundary_usage:read", + "boundary_usage:update", "coder:all", "coder:apikeys.manage_self", "coder:application_connect", @@ -4057,6 +4065,7 @@ export type RBACResource = | "assign_org_role" | "assign_role" | "audit_log" + | "boundary_usage" | "connection_log" | "crypto_key" | "debug_info" @@ -4101,6 +4110,7 @@ export const RBACResources: RBACResource[] = [ "assign_org_role", "assign_role", "audit_log", + "boundary_usage", "connection_log", "crypto_key", "debug_info",