From 17635dde5c99612b4aaf80970d49a116ed3fa29c Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Mon, 4 May 2026 15:42:34 -0500 Subject: [PATCH] chore: include pgcoordinator schema changes in 2.33 (#24931) Includes https://github.com/coder/coder/pull/24613 since it landed prior to the pgcoordinator migration --------- Co-authored-by: Marcin Tojek --- coderd/apidoc/docs.go | 8 ++ coderd/apidoc/swagger.json | 8 ++ coderd/database/dbauthz/dbauthz.go | 18 ++-- coderd/database/dbauthz/dbauthz_test.go | 11 ++- coderd/database/dbmetrics/querymetrics.go | 12 +-- coderd/database/dbmock/dbmock.go | 14 +-- coderd/database/dump.sql | 49 +---------- .../000482_add_ai_seat_scopes.down.sql | 2 + .../000482_add_ai_seat_scopes.up.sql | 3 + ...0483_drop_tailnet_notify_triggers.down.sql | 43 ++++++++++ ...000483_drop_tailnet_notify_triggers.up.sql | 6 ++ coderd/database/models.go | 11 ++- coderd/database/querier.go | 4 +- coderd/database/queries.sql.go | 59 +++++++++++-- coderd/database/queries/tailnet.sql | 10 ++- coderd/rbac/object_gen.go | 9 ++ coderd/rbac/policy/policy.go | 6 ++ coderd/rbac/roles.go | 6 +- coderd/rbac/roles_test.go | 8 ++ coderd/rbac/scopes_constants_gen.go | 6 ++ codersdk/apikey_scopes_gen.go | 3 + codersdk/rbacresources_gen.go | 2 + docs/reference/api/members.md | 40 ++++----- docs/reference/api/schemas.md | 12 +-- docs/reference/api/users.md | 10 +-- enterprise/aiseats/tracker_test.go | 85 ++++++++++++++----- enterprise/coderd/usage/cron_test.go | 20 +++-- enterprise/tailnet/pgcoord.go | 71 ++++++++++++++-- enterprise/tailnet/pgcoord_internal_test.go | 6 +- enterprise/tailnet/pgcoord_test.go | 11 ++- site/src/api/rbacresourcesGenerated.ts | 4 + site/src/api/typesGenerated.ts | 8 ++ 32 files changed, 408 insertions(+), 157 deletions(-) create mode 100644 coderd/database/migrations/000482_add_ai_seat_scopes.down.sql create mode 100644 coderd/database/migrations/000482_add_ai_seat_scopes.up.sql create mode 100644 coderd/database/migrations/000483_drop_tailnet_notify_triggers.down.sql create mode 100644 coderd/database/migrations/000483_drop_tailnet_notify_triggers.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 65f526f656..7965b568fd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -13798,6 +13798,9 @@ const docTemplate = `{ "enum": [ "all", "application_connect", + "ai_seat:*", + "ai_seat:create", + "ai_seat:read", "aibridge_interception:*", "aibridge_interception:create", "aibridge_interception:read", @@ -14007,6 +14010,9 @@ const docTemplate = `{ "x-enum-varnames": [ "APIKeyScopeAll", "APIKeyScopeApplicationConnect", + "APIKeyScopeAiSeatAll", + "APIKeyScopeAiSeatCreate", + "APIKeyScopeAiSeatRead", "APIKeyScopeAibridgeInterceptionAll", "APIKeyScopeAibridgeInterceptionCreate", "APIKeyScopeAibridgeInterceptionRead", @@ -19466,6 +19472,7 @@ const docTemplate = `{ "type": "string", "enum": [ "*", + "ai_seat", "aibridge_interception", "api_key", "assign_org_role", @@ -19512,6 +19519,7 @@ const docTemplate = `{ ], "x-enum-varnames": [ "ResourceWildcard", + "ResourceAiSeat", "ResourceAibridgeInterception", "ResourceApiKey", "ResourceAssignOrgRole", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 17d5c02c18..ec328932c4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12338,6 +12338,9 @@ "enum": [ "all", "application_connect", + "ai_seat:*", + "ai_seat:create", + "ai_seat:read", "aibridge_interception:*", "aibridge_interception:create", "aibridge_interception:read", @@ -12547,6 +12550,9 @@ "x-enum-varnames": [ "APIKeyScopeAll", "APIKeyScopeApplicationConnect", + "APIKeyScopeAiSeatAll", + "APIKeyScopeAiSeatCreate", + "APIKeyScopeAiSeatRead", "APIKeyScopeAibridgeInterceptionAll", "APIKeyScopeAibridgeInterceptionCreate", "APIKeyScopeAibridgeInterceptionRead", @@ -17808,6 +17814,7 @@ "type": "string", "enum": [ "*", + "ai_seat", "aibridge_interception", "api_key", "assign_org_role", @@ -17854,6 +17861,7 @@ ], "x-enum-varnames": [ "ResourceWildcard", + "ResourceAiSeat", "ResourceAibridgeInterception", "ResourceApiKey", "ResourceAssignOrgRole", diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1412054c5f..cd60befe57 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -226,6 +226,7 @@ var ( rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate}, rbac.ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceSystem.Type: {policy.WildcardSymbol}, + rbac.ResourceAiSeat.Type: {policy.ActionCreate}, // Required for UpsertAISeatState via SeatTracker. rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, // Unsure why provisionerd needs update and read personal rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, @@ -596,6 +597,7 @@ var ( DisplayName: "Usage Publisher", Site: rbac.Permissions(map[string][]policy.Action{ rbac.ResourceLicense.Type: {policy.ActionRead}, + rbac.ResourceAiSeat.Type: {policy.ActionRead}, // Required for GetActiveAISeatCount. // The usage publisher doesn't create events, just // reads/processes them. rbac.ResourceUsageEvent.Type: {policy.ActionRead, policy.ActionUpdate}, @@ -623,7 +625,7 @@ var ( }, rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys. rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - rbac.ResourceSystem.Type: {policy.ActionCreate}, // Required for UpsertAISeatState. + rbac.ResourceAiSeat.Type: {policy.ActionCreate}, // Required for UpsertAISeatState. }), User: []rbac.Permission{}, ByOrgID: map[string]rbac.OrgPermissions{}, @@ -1850,9 +1852,9 @@ func (q *querier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.U return q.db.DeleteAllChatQueuedMessages(ctx, chatID) } -func (q *querier) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) error { +func (q *querier) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) ([]database.DeleteAllTailnetTunnelsRow, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { - return err + return nil, err } return q.db.DeleteAllTailnetTunnels(ctx, arg) } @@ -2469,7 +2471,7 @@ func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Tim } func (q *querier) GetActiveAISeatCount(ctx context.Context) (int64, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceLicense); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAiSeat); err != nil { return 0, err } return q.db.GetActiveAISeatCount(ctx) @@ -4227,7 +4229,7 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License, } func (q *querier) GetUserAISeatStates(ctx context.Context, userIDs []uuid.UUID) ([]uuid.UUID, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAiSeat); err != nil { return nil, err } return q.db.GetUserAISeatStates(ctx, userIDs) @@ -6662,9 +6664,9 @@ func (q *querier) UpdateReplica(ctx context.Context, arg database.UpdateReplicaP return q.db.UpdateReplica(ctx, arg) } -func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) error { +func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil { - return err + return nil, err } return q.db.UpdateTailnetPeerStatusByCoordinator(ctx, arg) } @@ -7375,7 +7377,7 @@ func (q *querier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg datab } func (q *querier) UpsertAISeatState(ctx context.Context, arg database.UpsertAISeatStateParams) (bool, error) { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAiSeat); err != nil { return false, err } return q.db.UpsertAISeatState(ctx, arg) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 1ccb0bd4df..469d563650 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1848,15 +1848,18 @@ func (s *MethodTestSuite) TestProvisionerJob() { })) } -func (s *MethodTestSuite) TestLicense() { +func (s *MethodTestSuite) TestAISeat() { s.Run("GetActiveAISeatCount", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().GetActiveAISeatCount(gomock.Any()).Return(int64(100), nil).AnyTimes() - check.Args().Asserts(rbac.ResourceLicense, policy.ActionRead).Returns(int64(100)) + check.Args().Asserts(rbac.ResourceAiSeat, policy.ActionRead).Returns(int64(100)) })) s.Run("UpsertAISeatState", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().UpsertAISeatState(gomock.Any(), gomock.Any()).Return(true, nil).AnyTimes() - check.Args(database.UpsertAISeatStateParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate) + check.Args(database.UpsertAISeatStateParams{}).Asserts(rbac.ResourceAiSeat, policy.ActionCreate) })) +} + +func (s *MethodTestSuite) TestLicense() { s.Run("GetLicenses", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { a := database.License{ID: 1} b := database.License{ID: 2} @@ -2544,7 +2547,7 @@ func (s *MethodTestSuite) TestUser() { ids := []uuid.UUID{a.ID, b.ID} seatStates := []uuid.UUID{a.ID} dbm.EXPECT().GetUserAISeatStates(gomock.Any(), ids).Return(seatStates, nil).AnyTimes() - check.Args(ids).Asserts(rbac.ResourceUser, policy.ActionRead).Returns(seatStates) + check.Args(ids).Asserts(rbac.ResourceAiSeat, policy.ActionRead).Returns(seatStates) })) s.Run("GetUserByEmailOrUsername", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { u := testutil.Fake(s.T(), faker, database.User{}) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 5ca4e1d6cb..030c62ca63 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -400,12 +400,12 @@ func (m queryMetricsStore) DeleteAllChatQueuedMessages(ctx context.Context, chat return r0 } -func (m queryMetricsStore) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) error { +func (m queryMetricsStore) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) ([]database.DeleteAllTailnetTunnelsRow, error) { start := time.Now() - r0 := m.s.DeleteAllTailnetTunnels(ctx, arg) + r0, r1 := m.s.DeleteAllTailnetTunnels(ctx, arg) m.queryLatencies.WithLabelValues("DeleteAllTailnetTunnels").Observe(time.Since(start).Seconds()) m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAllTailnetTunnels").Inc() - return r0 + return r0, r1 } func (m queryMetricsStore) DeleteAllWebpushSubscriptions(ctx context.Context) error { @@ -4776,12 +4776,12 @@ func (m queryMetricsStore) UpdateReplica(ctx context.Context, arg database.Updat return r0, r1 } -func (m queryMetricsStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) error { +func (m queryMetricsStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) { start := time.Now() - r0 := m.s.UpdateTailnetPeerStatusByCoordinator(ctx, arg) + r0, r1 := m.s.UpdateTailnetPeerStatusByCoordinator(ctx, arg) m.queryLatencies.WithLabelValues("UpdateTailnetPeerStatusByCoordinator").Observe(time.Since(start).Seconds()) m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateTailnetPeerStatusByCoordinator").Inc() - return r0 + return r0, r1 } func (m queryMetricsStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) { diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index a9dacf2a93..0b1714aa4b 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -645,11 +645,12 @@ func (mr *MockStoreMockRecorder) DeleteAllChatQueuedMessages(ctx, chatID any) *g } // DeleteAllTailnetTunnels mocks base method. -func (m *MockStore) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) error { +func (m *MockStore) DeleteAllTailnetTunnels(ctx context.Context, arg database.DeleteAllTailnetTunnelsParams) ([]database.DeleteAllTailnetTunnelsRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAllTailnetTunnels", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].([]database.DeleteAllTailnetTunnelsRow) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteAllTailnetTunnels indicates an expected call of DeleteAllTailnetTunnels. @@ -9018,11 +9019,12 @@ func (mr *MockStoreMockRecorder) UpdateReplica(ctx, arg any) *gomock.Call { } // UpdateTailnetPeerStatusByCoordinator mocks base method. -func (m *MockStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) error { +func (m *MockStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg database.UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateTailnetPeerStatusByCoordinator", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].([]uuid.UUID) + ret1, _ := ret[1].(error) + return ret0, ret1 } // UpdateTailnetPeerStatusByCoordinator indicates an expected call of UpdateTailnetPeerStatusByCoordinator. diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index e168cee8fc..01ed07de6b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -220,7 +220,10 @@ CREATE TYPE api_key_scope AS ENUM ( 'chat:read', 'chat:update', 'chat:delete', - 'chat:*' + 'chat:*', + 'ai_seat:*', + 'ai_seat:create', + 'ai_seat:read' ); CREATE TYPE app_sharing_level AS ENUM ( @@ -1058,44 +1061,6 @@ BEGIN END; $$; -CREATE FUNCTION tailnet_notify_coordinator_heartbeat() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - PERFORM pg_notify('tailnet_coordinator_heartbeat', NEW.id::text); - RETURN NULL; -END; -$$; - -CREATE FUNCTION tailnet_notify_peer_change() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - IF (OLD IS NOT NULL) THEN - PERFORM pg_notify('tailnet_peer_update', OLD.id::text); - RETURN NULL; - END IF; - IF (NEW IS NOT NULL) THEN - PERFORM pg_notify('tailnet_peer_update', NEW.id::text); - RETURN NULL; - END IF; -END; -$$; - -CREATE FUNCTION tailnet_notify_tunnel_change() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - IF (NEW IS NOT NULL) THEN - PERFORM pg_notify('tailnet_tunnel_update', NEW.src_id || ',' || NEW.dst_id); - RETURN NULL; - ELSIF (OLD IS NOT NULL) THEN - PERFORM pg_notify('tailnet_tunnel_update', OLD.src_id || ',' || OLD.dst_id); - RETURN NULL; - END IF; -END; -$$; - CREATE TABLE ai_seat_state ( user_id uuid NOT NULL, first_used_at timestamp with time zone NOT NULL, @@ -4098,12 +4063,6 @@ CREATE TRIGGER remove_organization_member_custom_role BEFORE DELETE ON custom_ro COMMENT ON TRIGGER remove_organization_member_custom_role ON custom_roles IS 'When a custom_role is deleted, this trigger removes the role from all organization members.'; -CREATE TRIGGER tailnet_notify_coordinator_heartbeat AFTER INSERT OR UPDATE ON tailnet_coordinators FOR EACH ROW EXECUTE FUNCTION tailnet_notify_coordinator_heartbeat(); - -CREATE TRIGGER tailnet_notify_peer_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_peers FOR EACH ROW EXECUTE FUNCTION tailnet_notify_peer_change(); - -CREATE TRIGGER tailnet_notify_tunnel_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_tunnels FOR EACH ROW EXECUTE FUNCTION tailnet_notify_tunnel_change(); - CREATE TRIGGER trigger_aggregate_usage_event AFTER INSERT ON usage_events FOR EACH ROW EXECUTE FUNCTION aggregate_usage_event(); CREATE TRIGGER trigger_delete_group_members_on_org_member_delete BEFORE DELETE ON organization_members FOR EACH ROW EXECUTE FUNCTION delete_group_members_on_org_member_delete(); diff --git a/coderd/database/migrations/000482_add_ai_seat_scopes.down.sql b/coderd/database/migrations/000482_add_ai_seat_scopes.down.sql new file mode 100644 index 0000000000..6e4135fdcf --- /dev/null +++ b/coderd/database/migrations/000482_add_ai_seat_scopes.down.sql @@ -0,0 +1,2 @@ +-- These enum values cannot be removed from PostgreSQL. +-- This migration is a no-op placeholder for rollback safety. diff --git a/coderd/database/migrations/000482_add_ai_seat_scopes.up.sql b/coderd/database/migrations/000482_add_ai_seat_scopes.up.sql new file mode 100644 index 0000000000..52fa3e4b3a --- /dev/null +++ b/coderd/database/migrations/000482_add_ai_seat_scopes.up.sql @@ -0,0 +1,3 @@ +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_seat:*'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_seat:create'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_seat:read'; diff --git a/coderd/database/migrations/000483_drop_tailnet_notify_triggers.down.sql b/coderd/database/migrations/000483_drop_tailnet_notify_triggers.down.sql new file mode 100644 index 0000000000..ea0117340f --- /dev/null +++ b/coderd/database/migrations/000483_drop_tailnet_notify_triggers.down.sql @@ -0,0 +1,43 @@ +CREATE FUNCTION tailnet_notify_coordinator_heartbeat() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + PERFORM pg_notify('tailnet_coordinator_heartbeat', NEW.id::text); + RETURN NULL; +END; +$$; + +CREATE FUNCTION tailnet_notify_peer_change() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF (OLD IS NOT NULL) THEN + PERFORM pg_notify('tailnet_peer_update', OLD.id::text); + RETURN NULL; + END IF; + IF (NEW IS NOT NULL) THEN + PERFORM pg_notify('tailnet_peer_update', NEW.id::text); + RETURN NULL; + END IF; +END; +$$; + +CREATE FUNCTION tailnet_notify_tunnel_change() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + IF (NEW IS NOT NULL) THEN + PERFORM pg_notify('tailnet_tunnel_update', NEW.src_id || ',' || NEW.dst_id); + RETURN NULL; + ELSIF (OLD IS NOT NULL) THEN + PERFORM pg_notify('tailnet_tunnel_update', OLD.src_id || ',' || OLD.dst_id); + RETURN NULL; + END IF; +END; +$$; + +CREATE TRIGGER tailnet_notify_coordinator_heartbeat AFTER INSERT OR UPDATE ON tailnet_coordinators FOR EACH ROW EXECUTE FUNCTION tailnet_notify_coordinator_heartbeat(); + +CREATE TRIGGER tailnet_notify_peer_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_peers FOR EACH ROW EXECUTE FUNCTION tailnet_notify_peer_change(); + +CREATE TRIGGER tailnet_notify_tunnel_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_tunnels FOR EACH ROW EXECUTE FUNCTION tailnet_notify_tunnel_change(); diff --git a/coderd/database/migrations/000483_drop_tailnet_notify_triggers.up.sql b/coderd/database/migrations/000483_drop_tailnet_notify_triggers.up.sql new file mode 100644 index 0000000000..937a0c8ffd --- /dev/null +++ b/coderd/database/migrations/000483_drop_tailnet_notify_triggers.up.sql @@ -0,0 +1,6 @@ +DROP TRIGGER IF EXISTS tailnet_notify_peer_change ON tailnet_peers; +DROP TRIGGER IF EXISTS tailnet_notify_tunnel_change ON tailnet_tunnels; +DROP TRIGGER IF EXISTS tailnet_notify_coordinator_heartbeat ON tailnet_coordinators; +DROP FUNCTION IF EXISTS tailnet_notify_peer_change(); +DROP FUNCTION IF EXISTS tailnet_notify_tunnel_change(); +DROP FUNCTION IF EXISTS tailnet_notify_coordinator_heartbeat(); diff --git a/coderd/database/models.go b/coderd/database/models.go index 750f52bec8..65d3cfb2b4 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -224,6 +224,9 @@ const ( ApiKeyScopeChatUpdate APIKeyScope = "chat:update" ApiKeyScopeChatDelete APIKeyScope = "chat:delete" ApiKeyScopeChat APIKeyScope = "chat:*" + ApiKeyScopeAiSeat APIKeyScope = "ai_seat:*" + ApiKeyScopeAiSeatCreate APIKeyScope = "ai_seat:create" + ApiKeyScopeAiSeatRead APIKeyScope = "ai_seat:read" ) func (e *APIKeyScope) Scan(src interface{}) error { @@ -467,7 +470,10 @@ func (e APIKeyScope) Valid() bool { ApiKeyScopeChatRead, ApiKeyScopeChatUpdate, ApiKeyScopeChatDelete, - ApiKeyScopeChat: + ApiKeyScopeChat, + ApiKeyScopeAiSeat, + ApiKeyScopeAiSeatCreate, + ApiKeyScopeAiSeatRead: return true } return false @@ -680,6 +686,9 @@ func AllAPIKeyScopeValues() []APIKeyScope { ApiKeyScopeChatUpdate, ApiKeyScopeChatDelete, ApiKeyScopeChat, + ApiKeyScopeAiSeat, + ApiKeyScopeAiSeatCreate, + ApiKeyScopeAiSeatRead, } } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 5fb8b8baa7..d51d59a408 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -101,7 +101,7 @@ type sqlcQuerier interface { DeleteAPIKeyByID(ctx context.Context, id string) error DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error - DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error + DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) ([]DeleteAllTailnetTunnelsRow, error) // Deletes all existing webpush subscriptions. // This should be called when the VAPID keypair is regenerated, as the old // keypair will no longer be valid and all existing subscriptions will need to @@ -1112,7 +1112,7 @@ type sqlcQuerier interface { UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) - UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error + UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 27e5474f03..a9f677ea2e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -21234,10 +21234,11 @@ func (q *sqlQuerier) CleanTailnetTunnels(ctx context.Context) error { return err } -const deleteAllTailnetTunnels = `-- name: DeleteAllTailnetTunnels :exec +const deleteAllTailnetTunnels = `-- name: DeleteAllTailnetTunnels :many DELETE FROM tailnet_tunnels WHERE coordinator_id = $1 and src_id = $2 +RETURNING src_id, dst_id ` type DeleteAllTailnetTunnelsParams struct { @@ -21245,9 +21246,32 @@ type DeleteAllTailnetTunnelsParams struct { SrcID uuid.UUID `db:"src_id" json:"src_id"` } -func (q *sqlQuerier) DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error { - _, err := q.db.ExecContext(ctx, deleteAllTailnetTunnels, arg.CoordinatorID, arg.SrcID) - return err +type DeleteAllTailnetTunnelsRow struct { + SrcID uuid.UUID `db:"src_id" json:"src_id"` + DstID uuid.UUID `db:"dst_id" json:"dst_id"` +} + +func (q *sqlQuerier) DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) ([]DeleteAllTailnetTunnelsRow, error) { + rows, err := q.db.QueryContext(ctx, deleteAllTailnetTunnels, arg.CoordinatorID, arg.SrcID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DeleteAllTailnetTunnelsRow + for rows.Next() { + var i DeleteAllTailnetTunnelsRow + if err := rows.Scan(&i.SrcID, &i.DstID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const deleteTailnetPeer = `-- name: DeleteTailnetPeer :one @@ -21522,13 +21546,14 @@ func (q *sqlQuerier) GetTailnetTunnelPeerIDsBatch(ctx context.Context, ids []uui return items, nil } -const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :exec +const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :many UPDATE tailnet_peers SET status = $2 WHERE coordinator_id = $1 +RETURNING id ` type UpdateTailnetPeerStatusByCoordinatorParams struct { @@ -21536,9 +21561,27 @@ type UpdateTailnetPeerStatusByCoordinatorParams struct { Status TailnetStatus `db:"status" json:"status"` } -func (q *sqlQuerier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error { - _, err := q.db.ExecContext(ctx, updateTailnetPeerStatusByCoordinator, arg.CoordinatorID, arg.Status) - return err +func (q *sqlQuerier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) { + rows, err := q.db.QueryContext(ctx, updateTailnetPeerStatusByCoordinator, arg.CoordinatorID, arg.Status) + if err != nil { + return nil, err + } + defer rows.Close() + var items []uuid.UUID + for rows.Next() { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const upsertTailnetCoordinator = `-- name: UpsertTailnetCoordinator :one diff --git a/coderd/database/queries/tailnet.sql b/coderd/database/queries/tailnet.sql index 4620a31e6c..ce7cad98d6 100644 --- a/coderd/database/queries/tailnet.sql +++ b/coderd/database/queries/tailnet.sql @@ -50,13 +50,14 @@ DO UPDATE SET updated_at = now() at time zone 'utc' RETURNING *; --- name: UpdateTailnetPeerStatusByCoordinator :exec +-- name: UpdateTailnetPeerStatusByCoordinator :many UPDATE tailnet_peers SET status = $2 WHERE - coordinator_id = $1; + coordinator_id = $1 +RETURNING id; -- name: DeleteTailnetPeer :one DELETE @@ -91,10 +92,11 @@ FROM tailnet_tunnels WHERE coordinator_id = $1 and src_id = $2 and dst_id = $3 RETURNING coordinator_id, src_id, dst_id; --- name: DeleteAllTailnetTunnels :exec +-- name: DeleteAllTailnetTunnels :many DELETE FROM tailnet_tunnels -WHERE coordinator_id = $1 and src_id = $2; +WHERE coordinator_id = $1 and src_id = $2 +RETURNING src_id, dst_id; -- For PG Coordinator HTMLDebug diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index ded9be2820..338c454591 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -15,6 +15,14 @@ var ( Type: "*", } + // ResourceAiSeat + // Valid Actions + // - "ActionCreate" :: record AI seat usage + // - "ActionRead" :: read AI seat state + ResourceAiSeat = Object{ + Type: "ai_seat", + } + // ResourceAibridgeInterception // Valid Actions // - "ActionCreate" :: create aibridge interceptions & related records @@ -433,6 +441,7 @@ var ( func AllResources() []Objecter { return []Objecter{ ResourceWildcard, + ResourceAiSeat, ResourceAibridgeInterception, ResourceApiKey, ResourceAssignOrgRole, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 5ac669c127..c60bf10299 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -392,6 +392,12 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionCreate: "create aibridge interceptions & related records", }, }, + "ai_seat": { + Actions: map[Action]ActionDefinition{ + ActionCreate: "record AI seat usage", + ActionRead: "read AI seat state", + }, + }, "boundary_usage": { Actions: map[Action]ActionDefinition{ ActionRead: "read boundary usage statistics", diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 1a97fa594f..94ca6a875a 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -294,7 +294,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, ResourceBoundaryUsage), + allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUsageEvent, ResourceBoundaryUsage, ResourceAiSeat), // This adds back in the Workspace permissions. Permissions(map[string][]policy.Action{ ResourceWorkspace.Type: ownerWorkspaceActions, @@ -322,7 +322,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { denyPermissions..., ), User: append( - allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception, ResourceChat), + allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception, ResourceChat, ResourceAiSeat), Permissions(map[string][]policy.Action{ // Users cannot do create/update/delete on themselves, but they // can read their own details. @@ -454,7 +454,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // Org admins should not have workspace exec perms. organizationID.String(): { Org: append( - allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), + allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage, ResourceAiSeat), Permissions(map[string][]policy.Action{ ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH), ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 187ace42ca..212a1d48cd 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -1104,6 +1104,14 @@ func TestRolePermissions(t *testing.T) { false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, + { + Name: "AiSeat", + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, + Resource: rbac.ResourceAiSeat, + AuthorizeMap: map[bool][]hasAuthSubjects{ + false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + }, + }, { Name: "ChatUsageCRU", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, diff --git a/coderd/rbac/scopes_constants_gen.go b/coderd/rbac/scopes_constants_gen.go index 40f319a8ba..d94d0e5fd1 100644 --- a/coderd/rbac/scopes_constants_gen.go +++ b/coderd/rbac/scopes_constants_gen.go @@ -7,6 +7,8 @@ package rbac // declared in code, not here, to avoid duplication. const ( + ScopeAiSeatCreate ScopeName = "ai_seat:create" + ScopeAiSeatRead ScopeName = "ai_seat:read" ScopeAibridgeInterceptionCreate ScopeName = "aibridge_interception:create" ScopeAibridgeInterceptionRead ScopeName = "aibridge_interception:read" ScopeAibridgeInterceptionUpdate ScopeName = "aibridge_interception:update" @@ -171,6 +173,8 @@ func (e ScopeName) Valid() bool { case ScopeName("coder:all"), ScopeName("coder:application_connect"), ScopeName("no_user_data"), + ScopeAiSeatCreate, + ScopeAiSeatRead, ScopeAibridgeInterceptionCreate, ScopeAibridgeInterceptionRead, ScopeAibridgeInterceptionUpdate, @@ -336,6 +340,8 @@ func AllScopeNameValues() []ScopeName { ScopeName("coder:all"), ScopeName("coder:application_connect"), ScopeName("no_user_data"), + ScopeAiSeatCreate, + ScopeAiSeatRead, ScopeAibridgeInterceptionCreate, ScopeAibridgeInterceptionRead, ScopeAibridgeInterceptionUpdate, diff --git a/codersdk/apikey_scopes_gen.go b/codersdk/apikey_scopes_gen.go index 53dab5bd28..dd3a94bb3c 100644 --- a/codersdk/apikey_scopes_gen.go +++ b/codersdk/apikey_scopes_gen.go @@ -6,6 +6,9 @@ const ( APIKeyScopeAll APIKeyScope = "all" // Deprecated: use codersdk.APIKeyScopeCoderApplicationConnect instead. APIKeyScopeApplicationConnect APIKeyScope = "application_connect" + APIKeyScopeAiSeatAll APIKeyScope = "ai_seat:*" + APIKeyScopeAiSeatCreate APIKeyScope = "ai_seat:create" + APIKeyScopeAiSeatRead APIKeyScope = "ai_seat:read" APIKeyScopeAibridgeInterceptionAll APIKeyScope = "aibridge_interception:*" APIKeyScopeAibridgeInterceptionCreate APIKeyScope = "aibridge_interception:create" APIKeyScopeAibridgeInterceptionRead APIKeyScope = "aibridge_interception:read" diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 724e265de5..833af15f56 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -5,6 +5,7 @@ type RBACResource string const ( ResourceWildcard RBACResource = "*" + ResourceAiSeat RBACResource = "ai_seat" ResourceAibridgeInterception RBACResource = "aibridge_interception" ResourceApiKey RBACResource = "api_key" ResourceAssignOrgRole RBACResource = "assign_org_role" @@ -77,6 +78,7 @@ const ( // said resource type. var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceWildcard: {}, + ResourceAiSeat: {ActionCreate, ActionRead}, ResourceAibridgeInterception: {ActionCreate, ActionRead, ActionUpdate}, ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUnassign, ActionUpdate}, diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 96a30b0fa2..804b143f0f 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -193,10 +193,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_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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). @@ -326,10 +326,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_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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). @@ -459,10 +459,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_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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). @@ -554,10 +554,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_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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). @@ -960,9 +960,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_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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 e84f954e7c..f16b092b1e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1387,9 +1387,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`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat: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:update_agent`, `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_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | +| Value(s) | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ai_seat:*`, `ai_seat:create`, `ai_seat:read`, `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`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat: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:update_agent`, `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_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | ## codersdk.AddLicenseRequest @@ -8272,9 +8272,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit| #### Enumerated Values -| Value(s) | -|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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) | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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 01187a8cfb..db0a23b1ef 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -853,11 +853,11 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | `*`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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` | `*`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `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/enterprise/aiseats/tracker_test.go b/enterprise/aiseats/tracker_test.go index c6f2750eb4..37e192cd4b 100644 --- a/enterprise/aiseats/tracker_test.go +++ b/enterprise/aiseats/tracker_test.go @@ -23,21 +23,31 @@ import ( "github.com/coder/quartz" ) +// authzSetup returns a raw DB for seeding and an RBAC-wrapped DB +// that enforces real authorization checks. +func authzSetup(t *testing.T) (rawDB database.Store, authzDB database.Store) { + t.Helper() + rawDB, _ = dbtestutil.NewDB(t) + authz := rbac.NewStrictAuthorizer(prometheus.NewRegistry()) + authzDB = dbauthz.New(rawDB, authz, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer()) + return rawDB, authzDB +} + func TestSeatTrackerDB(t *testing.T) { t.Parallel() t.Run("ActiveUserRecorded", func(t *testing.T) { t.Parallel() - db, _ := dbtestutil.NewDB(t) + rawDB, authzDB := authzSetup(t) ctx := testutil.Context(t, testutil.WaitShort) clock := quartz.NewMock(t) - tracker := enterpriseaiseats.New(db, testutil.Logger(t), clock, nil) + tracker := enterpriseaiseats.New(authzDB, testutil.Logger(t), clock, nil) - user := dbgen.User(t, db, database.User{Status: database.UserStatusActive}) - tracker.RecordUsage(ctx, user.ID, agplaiseats.ReasonAIBridge("active user event")) + user := dbgen.User(t, rawDB, database.User{Status: database.UserStatusActive}) + tracker.RecordUsage(dbauthz.AsAIBridged(ctx), user.ID, agplaiseats.ReasonAIBridge("active user event")) - count, err := db.GetActiveAISeatCount(ctx) + count, err := rawDB.GetActiveAISeatCount(ctx) require.NoError(t, err) require.EqualValues(t, 1, count) }) @@ -77,17 +87,17 @@ func TestSeatTrackerDB(t *testing.T) { t.Run("InactiveUsersExcluded", func(t *testing.T) { t.Parallel() - db, _ := dbtestutil.NewDB(t) + rawDB, authzDB := authzSetup(t) ctx := testutil.Context(t, testutil.WaitShort) - tracker := enterpriseaiseats.New(db, testutil.Logger(t), quartz.NewMock(t), nil) + tracker := enterpriseaiseats.New(authzDB, testutil.Logger(t), quartz.NewMock(t), nil) - dormantUser := dbgen.User(t, db, database.User{Status: database.UserStatusDormant}) - tracker.RecordUsage(ctx, dormantUser.ID, agplaiseats.ReasonTask("dormant user event")) + dormantUser := dbgen.User(t, rawDB, database.User{Status: database.UserStatusDormant}) + tracker.RecordUsage(dbauthz.AsAIBridged(ctx), dormantUser.ID, agplaiseats.ReasonTask("dormant user event")) - suspendedUser := dbgen.User(t, db, database.User{Status: database.UserStatusSuspended}) - tracker.RecordUsage(ctx, suspendedUser.ID, agplaiseats.ReasonTask("suspended user event")) + suspendedUser := dbgen.User(t, rawDB, database.User{Status: database.UserStatusSuspended}) + tracker.RecordUsage(dbauthz.AsAIBridged(ctx), suspendedUser.ID, agplaiseats.ReasonTask("suspended user event")) - count, err := db.GetActiveAISeatCount(ctx) + count, err := rawDB.GetActiveAISeatCount(ctx) require.NoError(t, err) require.EqualValues(t, 0, count) }) @@ -95,23 +105,23 @@ func TestSeatTrackerDB(t *testing.T) { t.Run("StatusTransitions", func(t *testing.T) { t.Parallel() - db, _ := dbtestutil.NewDB(t) + rawDB, authzDB := authzSetup(t) ctx := testutil.Context(t, testutil.WaitShort) a := audit.NewMock() var aI audit.Auditor = a var al atomic.Pointer[audit.Auditor] al.Store(&aI) - tracker := enterpriseaiseats.New(db, testutil.Logger(t), quartz.NewMock(t), &al) + tracker := enterpriseaiseats.New(authzDB, testutil.Logger(t), quartz.NewMock(t), &al) - user := dbgen.User(t, db, database.User{Status: database.UserStatusActive}) - tracker.RecordUsage(ctx, user.ID, agplaiseats.ReasonAIBridge("status transition")) + user := dbgen.User(t, rawDB, database.User{Status: database.UserStatusActive}) + tracker.RecordUsage(dbauthz.AsAIBridged(ctx), user.ID, agplaiseats.ReasonAIBridge("status transition")) - count, err := db.GetActiveAISeatCount(ctx) + count, err := rawDB.GetActiveAISeatCount(ctx) require.NoError(t, err) require.EqualValues(t, 1, count) - _, err = db.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ + _, err = rawDB.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ ID: user.ID, Status: database.UserStatusDormant, UpdatedAt: dbtime.Now(), @@ -119,11 +129,11 @@ func TestSeatTrackerDB(t *testing.T) { }) require.NoError(t, err) - count, err = db.GetActiveAISeatCount(ctx) + count, err = rawDB.GetActiveAISeatCount(ctx) require.NoError(t, err) require.EqualValues(t, 0, count) - _, err = db.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ + _, err = rawDB.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ ID: user.ID, Status: database.UserStatusActive, UpdatedAt: dbtime.Now().Add(time.Second), @@ -131,11 +141,44 @@ func TestSeatTrackerDB(t *testing.T) { }) require.NoError(t, err) - count, err = db.GetActiveAISeatCount(ctx) + count, err = rawDB.GetActiveAISeatCount(ctx) require.NoError(t, err) require.EqualValues(t, 1, count) require.Len(t, a.AuditLogs(), 1) require.Equal(t, database.ResourceTypeAiSeat, a.AuditLogs()[0].ResourceType) }) + + // Provisionerd also calls RecordUsage via SeatTracker for + // task workspace builds. + t.Run("AsProvisionerd", func(t *testing.T) { + t.Parallel() + + rawDB, authzDB := authzSetup(t) + ctx := testutil.Context(t, testutil.WaitShort) + tracker := enterpriseaiseats.New(authzDB, testutil.Logger(t), quartz.NewMock(t), nil) + + user := dbgen.User(t, rawDB, database.User{Status: database.UserStatusActive}) + tracker.RecordUsage(dbauthz.AsProvisionerd(ctx), user.ID, agplaiseats.ReasonTask("task build")) + + count, err := rawDB.GetActiveAISeatCount(ctx) + require.NoError(t, err) + require.EqualValues(t, 1, count) + }) + + // AsUsagePublisher reads AI seat count in heartbeats. + t.Run("AsUsagePublisher", func(t *testing.T) { + t.Parallel() + + rawDB, authzDB := authzSetup(t) + ctx := testutil.Context(t, testutil.WaitShort) + tracker := enterpriseaiseats.New(authzDB, testutil.Logger(t), quartz.NewMock(t), nil) + + user := dbgen.User(t, rawDB, database.User{Status: database.UserStatusActive}) + tracker.RecordUsage(dbauthz.AsAIBridged(ctx), user.ID, agplaiseats.ReasonAIBridge("heartbeat test")) + + count, err := authzDB.GetActiveAISeatCount(dbauthz.AsUsagePublisher(ctx)) + require.NoError(t, err) + require.EqualValues(t, 1, count) + }) } diff --git a/enterprise/coderd/usage/cron_test.go b/enterprise/coderd/usage/cron_test.go index c2cf9e44d9..8381e6e77f 100644 --- a/enterprise/coderd/usage/cron_test.go +++ b/enterprise/coderd/usage/cron_test.go @@ -5,13 +5,17 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "cdr.dev/slog/v3/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/usage/usagetypes" "github.com/coder/coder/v2/enterprise/coderd/usage" "github.com/coder/coder/v2/testutil" @@ -77,21 +81,27 @@ func TestCron(t *testing.T) { } // TestAISeatsHeartbeat checks that AISeatsHeartbeat returns the -// correct event type and count. +// correct event type and count. It wraps a mock database with dbauthz +// to verify that the AsUsagePublisher subject has the required +// ResourceAiSeat.ActionRead permission. func TestAISeatsHeartbeat(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitLong) ctrl := gomock.NewController(t) db := dbmock.NewMockStore(ctrl) + db.EXPECT().Wrappers().Return([]string{}).AnyTimes() db.EXPECT().GetActiveAISeatCount(gomock.Any()).Return(int64(42), nil) - fn := usage.AISeatsHeartbeat(db) - event, err := fn(ctx) + authz := rbac.NewStrictAuthorizer(prometheus.NewRegistry()) + authzDB := dbauthz.New(db, authz, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer()) + + // AISeatsHeartbeat internally uses AsUsagePublisher, which must + // have ResourceAiSeat.ActionRead to pass the dbauthz check. + fn := usage.AISeatsHeartbeat(authzDB) + event, err := fn(testutil.Context(t, testutil.WaitLong)) require.NoError(t, err) - // Verify the event type and count. hb, ok := event.(usagetypes.HBAISeats) require.True(t, ok) assert.Equal(t, int64(42), hb.Count) diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go index 81dc970f13..6f8abd701c 100644 --- a/enterprise/tailnet/pgcoord.go +++ b/enterprise/tailnet/pgcoord.go @@ -42,6 +42,27 @@ const ( CloseErrUnhealthy = "coordinator unhealthy" ) +func publishPeerUpdate(ctx context.Context, ps pubsub.Pubsub, logger slog.Logger, peerID uuid.UUID) { + if err := ps.Publish(eventPeerUpdate, []byte(peerID.String())); err != nil { + logger.Warn(ctx, "failed to publish peer update", slog.F("peer_id", peerID), slog.Error(err)) + } +} + +func publishTunnelUpdate(ctx context.Context, ps pubsub.Pubsub, logger slog.Logger, srcID, dstID uuid.UUID) { + if err := ps.Publish(eventTunnelUpdate, []byte(srcID.String()+","+dstID.String())); err != nil { + logger.Warn(ctx, "failed to publish tunnel update", + slog.F("src_id", srcID), slog.F("dst_id", dstID), slog.Error(err)) + } +} + +func publishCoordinatorHeartbeat(ctx context.Context, ps pubsub.Pubsub, logger slog.Logger, id uuid.UUID) { + if err := ps.Publish(EventHeartbeats, []byte(id.String())); err != nil { + logger.Warn(ctx, "failed to publish coordinator heartbeat", slog.F("coordinator_id", id), slog.Error(err)) + } else { + logger.Debug(ctx, "sent heartbeat", slog.F("coordinator_id", id)) + } +} + // pgCoord is a postgres-backed coordinator // // ┌────────────┐ @@ -152,11 +173,11 @@ func newPGCoordInternal( logger: logger, pubsub: ps, store: store, - binder: newBinder(ctx, logger, id, store, bCh, fHB), + binder: newBinder(ctx, logger, id, store, ps, bCh, fHB), bindings: bCh, newConnections: cCh, closeConnections: ccCh, - tunneler: newTunneler(ctx, logger, id, store, sCh, fHB), + tunneler: newTunneler(ctx, logger, id, store, ps, sCh, fHB), tunnelerCh: sCh, handshaker: newHandshaker(ctx, logger, id, ps, rfhCh, fHB), handshakerCh: rfhCh, @@ -273,6 +294,7 @@ type tunneler struct { logger slog.Logger coordinatorID uuid.UUID store database.Store + pubsub pubsub.Pubsub updates <-chan tunnel mu sync.Mutex @@ -286,6 +308,7 @@ func newTunneler(ctx context.Context, logger slog.Logger, id uuid.UUID, store database.Store, + ps pubsub.Pubsub, updates <-chan tunnel, startWorkers <-chan struct{}, ) *tunneler { @@ -294,6 +317,7 @@ func newTunneler(ctx context.Context, logger: logger, coordinatorID: id, store: store, + pubsub: ps, updates: updates, latest: make(map[uuid.UUID]map[uuid.UUID]tunnel), workQ: newWorkQ[tKey](ctx), @@ -396,7 +420,8 @@ func (t *tunneler) writeOne(tun tunnel) error { var err error switch { case tun.dst == uuid.Nil: - err = t.store.DeleteAllTailnetTunnels(t.ctx, database.DeleteAllTailnetTunnelsParams{ + var deleted []database.DeleteAllTailnetTunnelsRow + deleted, err = t.store.DeleteAllTailnetTunnels(t.ctx, database.DeleteAllTailnetTunnelsParams{ SrcID: tun.src, CoordinatorID: t.coordinatorID, }) @@ -404,6 +429,11 @@ func (t *tunneler) writeOne(tun tunnel) error { slog.F("src_id", tun.src), slog.Error(err), ) + if err == nil { + for _, row := range deleted { + publishTunnelUpdate(t.ctx, t.pubsub, t.logger, row.SrcID, row.DstID) + } + } case tun.active: _, err = t.store.UpsertTailnetTunnel(t.ctx, database.UpsertTailnetTunnelParams{ CoordinatorID: t.coordinatorID, @@ -415,6 +445,9 @@ func (t *tunneler) writeOne(tun tunnel) error { slog.F("dst_id", tun.dst), slog.Error(err), ) + if err == nil { + publishTunnelUpdate(t.ctx, t.pubsub, t.logger, tun.src, tun.dst) + } case !tun.active: _, err = t.store.DeleteTailnetTunnel(t.ctx, database.DeleteTailnetTunnelParams{ CoordinatorID: t.coordinatorID, @@ -428,7 +461,10 @@ func (t *tunneler) writeOne(tun tunnel) error { ) // writeOne should be idempotent if xerrors.Is(err, sql.ErrNoRows) { - err = nil + return nil // No row deleted, skip publish. + } + if err == nil { + publishTunnelUpdate(t.ctx, t.pubsub, t.logger, tun.src, tun.dst) } default: panic("unreachable") @@ -459,6 +495,7 @@ type binder struct { logger slog.Logger coordinatorID uuid.UUID store database.Store + pubsub pubsub.Pubsub bindings <-chan binding mu sync.Mutex @@ -473,6 +510,7 @@ func newBinder(ctx context.Context, logger slog.Logger, id uuid.UUID, store database.Store, + ps pubsub.Pubsub, bindings <-chan binding, startWorkers <-chan struct{}, ) *binder { @@ -481,6 +519,7 @@ func newBinder(ctx context.Context, logger: logger, coordinatorID: id, store: store, + pubsub: ps, bindings: bindings, latest: make(map[bKey]binding), workQ: newWorkQ[bKey](ctx), @@ -508,13 +547,16 @@ func newBinder(ctx context.Context, ctx, cancel := context.WithTimeout(dbauthz.As(context.Background(), pgCoordSubject), time.Second*15) defer cancel() - err := b.store.UpdateTailnetPeerStatusByCoordinator(ctx, database.UpdateTailnetPeerStatusByCoordinatorParams{ + peerIDs, err := b.store.UpdateTailnetPeerStatusByCoordinator(ctx, database.UpdateTailnetPeerStatusByCoordinatorParams{ CoordinatorID: b.coordinatorID, Status: database.TailnetStatusLost, }) if err != nil { b.logger.Error(b.ctx, "update peer status to lost", slog.Error(err)) } + for _, peerID := range peerIDs { + publishPeerUpdate(ctx, b.pubsub, b.logger, peerID) + } }() return b } @@ -593,6 +635,9 @@ func (b *binder) writeOne(bnd binding) error { slog.F("node", bnd.node), slog.Error(err)) } + if err == nil { + publishPeerUpdate(b.ctx, b.pubsub, b.logger, uuid.UUID(bnd.bKey)) + } return err } @@ -1299,9 +1344,11 @@ func (q *querier) listenReadyForHandshake(_ context.Context, msg []byte, err err func (q *querier) resyncPeerMappings() { q.mu.Lock() defer q.mu.Unlock() + keys := make([]mKey, 0, len(q.mappers)) for mk := range q.mappers { - q.mappingQ.enqueue(mk) + keys = append(keys, mk) } + q.mappingQ.enqueue(keys...) } func (q *querier) handleUpdates() { @@ -1710,11 +1757,17 @@ func (h *heartbeats) checkExpiry() { expired := false for id, t := range h.coordinators { lastHB := now.Sub(t) - h.logger.Debug(h.ctx, "last heartbeat from coordinator", slog.F("other_coordinator_id", id), slog.F("last_heartbeat", lastHB)) + h.logger.Debug(h.ctx, "last heartbeat from coordinator", + slog.F("other_coordinator_id", id), + slog.F("last_heartbeat", lastHB), + ) if lastHB >= MissedHeartbeats*HeartbeatPeriod { expired = true delete(h.coordinators, id) - h.logger.Info(h.ctx, "coordinator failed heartbeat check", slog.F("other_coordinator_id", id), slog.F("last_heartbeat", lastHB)) + h.logger.Info(h.ctx, "coordinator failed heartbeat check", + slog.F("other_coordinator_id", id), + slog.F("last_heartbeat", lastHB), + ) } } if expired { @@ -1754,7 +1807,7 @@ func (h *heartbeats) sendBeat() { } return } - h.logger.Debug(h.ctx, "sent heartbeat") + publishCoordinatorHeartbeat(h.ctx, h.pubsub, h.logger, h.self) if h.failedHeartbeats >= 3 { h.logger.Info(h.ctx, "coordinator sent heartbeat and is healthy") _ = agpl.SendCtx(h.ctx, h.update, hbUpdate{health: healthUpdateHealthy}) diff --git a/enterprise/tailnet/pgcoord_internal_test.go b/enterprise/tailnet/pgcoord_internal_test.go index 3c9ad786f7..975e499278 100644 --- a/enterprise/tailnet/pgcoord_internal_test.go +++ b/enterprise/tailnet/pgcoord_internal_test.go @@ -76,6 +76,8 @@ func TestHeartbeats_recvBeat_resetSkew(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) logger := testutil.Logger(t) + ctrl := gomock.NewController(t) + mStore := dbmock.NewMockStore(ctrl) mClock := quartz.NewMock(t) trap := mClock.Trap().Until("heartbeats", "resetExpiryTimerWithLock") defer trap.Close() @@ -83,12 +85,12 @@ func TestHeartbeats_recvBeat_resetSkew(t *testing.T) { uut := heartbeats{ ctx: ctx, logger: logger, + store: mStore, clock: mClock, self: uuid.UUID{1}, update: make(chan hbUpdate, 4), coordinators: make(map[uuid.UUID]time.Time), } - coord2 := uuid.UUID{2} coord3 := uuid.UUID{3} @@ -397,7 +399,7 @@ func TestPGCoordinatorUnhealthy(t *testing.T) { mStore.EXPECT().CleanTailnetCoordinators(gomock.Any()).AnyTimes().Return(nil) mStore.EXPECT().CleanTailnetLostPeers(gomock.Any()).AnyTimes().Return(nil) mStore.EXPECT().CleanTailnetTunnels(gomock.Any()).AnyTimes().Return(nil) - mStore.EXPECT().UpdateTailnetPeerStatusByCoordinator(gomock.Any(), gomock.Any()) + mStore.EXPECT().UpdateTailnetPeerStatusByCoordinator(gomock.Any(), gomock.Any()).Return(nil, nil) coordinator, err := newPGCoordInternal(ctx, logger, ps, mStore, mClock) require.NoError(t, err) diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go index ccb1fe2016..3ec874ad17 100644 --- a/enterprise/tailnet/pgcoord_test.go +++ b/enterprise/tailnet/pgcoord_test.go @@ -268,6 +268,7 @@ func TestPGCoordinatorSingle_MissedHeartbeats(t *testing.T) { ctx: ctx, t: t, store: store, + ps: ps, id: uuid.New(), } @@ -281,6 +282,7 @@ func TestPGCoordinatorSingle_MissedHeartbeats(t *testing.T) { ctx: ctx, t: t, store: store, + ps: ps, id: uuid.New(), } fCoord3.heartbeat() @@ -304,7 +306,6 @@ func TestPGCoordinatorSingle_MissedHeartbeats(t *testing.T) { // one more heartbeat period will result in fCoord2 being expired, which should cause us to // revert to the original agent mapping mClock.Advance(tailnet.HeartbeatPeriod).MustWait(ctx) - // note that the timeout doesn't get reset because both fCoord2 and fCoord3 are expired client.AssertEventuallyHasDERP(agent.ID, 10) // send fCoord3 heartbeat, which should trigger us to consider that mapping valid again. @@ -343,6 +344,7 @@ func TestPGCoordinatorSingle_MissedHeartbeats_NoDrop(t *testing.T) { ctx: ctx, t: t, store: store, + ps: ps, id: uuid.New(), } // simulate a single heartbeat, the coordinator is healthy @@ -594,7 +596,7 @@ func TestPGCoordinator_Unhealthy(t *testing.T) { mStore.EXPECT().GetTailnetTunnelPeerBindingsBatch(gomock.Any(), gomock.Any()).AnyTimes().Return(nil, nil) mStore.EXPECT().DeleteTailnetPeer(gomock.Any(), gomock.Any()). AnyTimes().Return(database.DeleteTailnetPeerRow{}, nil) - mStore.EXPECT().DeleteAllTailnetTunnels(gomock.Any(), gomock.Any()).AnyTimes().Return(nil) + mStore.EXPECT().DeleteAllTailnetTunnels(gomock.Any(), gomock.Any()).AnyTimes().Return(nil, nil) mStore.EXPECT().UpdateTailnetPeerStatusByCoordinator(gomock.Any(), gomock.Any()) uut, err := tailnet.NewPGCoord(ctx, logger, ps, mStore) @@ -948,6 +950,7 @@ type fakeCoordinator struct { ctx context.Context t *testing.T store database.Store + ps pubsub.Pubsub id uuid.UUID } @@ -955,6 +958,8 @@ func (c *fakeCoordinator) heartbeat() { c.t.Helper() _, err := c.store.UpsertTailnetCoordinator(c.ctx, c.id) require.NoError(c.t, err) + err = c.ps.Publish(tailnet.EventHeartbeats, []byte(c.id.String())) + require.NoError(c.t, err) } func (c *fakeCoordinator) agentNode(agentID uuid.UUID, node *agpl.Node) { @@ -970,4 +975,6 @@ func (c *fakeCoordinator) agentNode(agentID uuid.UUID, node *agpl.Node) { Status: database.TailnetStatusOk, }) require.NoError(c.t, err) + err = c.ps.Publish("tailnet_peer_update", []byte(agentID.String())) + require.NoError(c.t, err) } diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 66a18b9999..a2cad73aa1 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -8,6 +8,10 @@ import type { RBACAction, RBACResource } from "./typesGenerated"; export const RBACResourceActions: Partial< Record>> > = { + ai_seat: { + create: "record AI seat usage", + read: "read AI seat state", + }, aibridge_interception: { create: "create aibridge interceptions & related records", read: "read aibridge interceptions & related records", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 304ead89d0..95f6a01f85 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -339,6 +339,9 @@ export interface APIKey { // From codersdk/apikey.go export type APIKeyScope = + | "ai_seat:*" + | "ai_seat:create" + | "ai_seat:read" | "aibridge_interception:*" | "aibridge_interception:create" | "aibridge_interception:read" @@ -548,6 +551,9 @@ export type APIKeyScope = | "workspace:update_agent"; export const APIKeyScopes: APIKeyScope[] = [ + "ai_seat:*", + "ai_seat:create", + "ai_seat:read", "aibridge_interception:*", "aibridge_interception:create", "aibridge_interception:read", @@ -6131,6 +6137,7 @@ export const RBACActions: RBACAction[] = [ // From codersdk/rbacresources_gen.go export type RBACResource = + | "ai_seat" | "aibridge_interception" | "api_key" | "assign_org_role" @@ -6177,6 +6184,7 @@ export type RBACResource = | "workspace_proxy"; export const RBACResources: RBACResource[] = [ + "ai_seat", "aibridge_interception", "api_key", "assign_org_role",