From 6019ded6edc3a20972bec28bea708ea887b28ac8 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 9 Jul 2024 19:17:08 +0000 Subject: [PATCH 01/20] chore: add provisioner key crud apis --- coderd/apidoc/docs.go | 2 + coderd/apidoc/swagger.json | 2 + coderd/database/dbauthz/dbauthz.go | 28 ++++ coderd/database/dbmem/dbmem.go | 74 ++++++++++ coderd/database/dbmetrics/dbmetrics.go | 28 ++++ coderd/database/dbmock/dbmock.go | 59 ++++++++ coderd/database/dump.sql | 17 +++ coderd/database/foreign_key_constraint.go | 1 + .../000223_provisioner_keys.down.sql | 1 + .../migrations/000223_provisioner_keys.up.sql | 8 ++ coderd/database/modelmethods.go | 6 + coderd/database/models.go | 8 ++ coderd/database/querier.go | 4 + coderd/database/queries.sql.go | 128 ++++++++++++++++++ coderd/database/queries/provisionerkeys.sql | 38 ++++++ coderd/database/unique_constraint.go | 2 + coderd/rbac/object_gen.go | 10 ++ coderd/rbac/policy/policy.go | 7 + codersdk/rbacresources_gen.go | 2 + docs/api/members.md | 3 + docs/api/schemas.md | 1 + site/src/api/typesGenerated.ts | 2 + 22 files changed, 431 insertions(+) create mode 100644 coderd/database/migrations/000223_provisioner_keys.down.sql create mode 100644 coderd/database/migrations/000223_provisioner_keys.up.sql create mode 100644 coderd/database/queries/provisionerkeys.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0aecfdea8ff5e..353f1584efb33 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10897,6 +10897,7 @@ const docTemplate = `{ "organization", "organization_member", "provisioner_daemon", + "provisioner_keys", "replicas", "system", "tailnet_coordinator", @@ -10924,6 +10925,7 @@ const docTemplate = `{ "ResourceOrganization", "ResourceOrganizationMember", "ResourceProvisionerDaemon", + "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6d5ac99848dbf..597facb6e3cad 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9819,6 +9819,7 @@ "organization", "organization_member", "provisioner_daemon", + "provisioner_keys", "replicas", "system", "tailnet_coordinator", @@ -9846,6 +9847,7 @@ "ResourceOrganization", "ResourceOrganizationMember", "ResourceProvisionerDaemon", + "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 85df46dce620c..3abb9fc6ce405 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1074,6 +1074,13 @@ func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.Del }, q.db.DeleteOrganizationMember)(ctx, arg) } +func (q *querier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error { + if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceProvisionerKeys); err != nil { + return err + } + return q.db.DeleteProvisionerKey(ctx, id) +} + func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err @@ -1671,6 +1678,13 @@ func (q *querier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt return q.db.GetProvisionerJobsCreatedAfter(ctx, createdAt) } +func (q *querier) GetProvisionerKeyByName(ctx context.Context, name database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerKeys); err != nil { + return database.ProvisionerKey{}, err + } + return q.db.GetProvisionerKeyByName(ctx, name) +} + func (q *querier) GetProvisionerLogsAfterID(ctx context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { // Authorized read on job lets the actor also read the logs. _, err := q.GetProvisionerJobByID(ctx, arg.JobID) @@ -2615,6 +2629,13 @@ func (q *querier) InsertProvisionerJobLogs(ctx context.Context, arg database.Ins return q.db.InsertProvisionerJobLogs(ctx, arg) } +func (q *querier) InsertProvisionerKey(ctx context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceProvisionerKeys); err != nil { + return database.ProvisionerKey{}, err + } + return q.db.InsertProvisionerKey(ctx, arg) +} + func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.Replica{}, err @@ -2843,6 +2864,13 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab return q.db.InsertWorkspaceResourceMetadata(ctx, arg) } +func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerKeys); err != nil { + return nil, err + } + return q.db.ListProvisionerKeysByOrganization(ctx, organizationID) +} + func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID) if err != nil { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9effbe1bb69f2..32d47d5124963 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -168,6 +168,7 @@ type data struct { provisionerDaemons []database.ProvisionerDaemon provisionerJobLogs []database.ProvisionerJobLog provisionerJobs []database.ProvisionerJob + provisionerKeys []database.ProvisionerKey replicas []database.Replica templateVersions []database.TemplateVersionTable templateVersionParameters []database.TemplateVersionParameter @@ -1734,6 +1735,20 @@ func (q *FakeQuerier) DeleteOrganizationMember(_ context.Context, arg database.D return nil } +func (q *FakeQuerier) DeleteProvisionerKey(_ context.Context, id uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, key := range q.provisionerKeys { + if key.ID == id { + q.provisionerKeys = append(q.provisionerKeys[:i], q.provisionerKeys[i+1:]...) + return nil + } + } + + return sql.ErrNoRows +} + func (q *FakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -3195,6 +3210,19 @@ func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after ti return jobs, nil } +func (q *FakeQuerier) GetProvisionerKeyByName(ctx context.Context, arg database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, key := range q.provisionerKeys { + if key.Name == arg.Name && key.OrganizationID == arg.OrganizationID { + return key, nil + } + } + + return database.ProvisionerKey{}, sql.ErrNoRows +} + func (q *FakeQuerier) GetProvisionerLogsAfterID(_ context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { if err := validateDatabaseType(arg); err != nil { return nil, err @@ -6493,6 +6521,33 @@ func (q *FakeQuerier) InsertProvisionerJobLogs(_ context.Context, arg database.I return logs, nil } +func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.ProvisionerKey{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, key := range q.provisionerKeys { + if key.ID == arg.ID || (key.OrganizationID == arg.OrganizationID && key.Name == arg.Name) { + return database.ProvisionerKey{}, errUniqueConstraint + } + } + + provisionerKey := database.ProvisionerKey{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + OrganizationID: arg.OrganizationID, + Name: arg.Name, + HashedSecret: []byte(arg.Name), + } + q.provisionerKeys = append(q.provisionerKeys, provisionerKey) + + return provisionerKey, nil +} + func (q *FakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplicaParams) (database.Replica, error) { if err := validateDatabaseType(arg); err != nil { return database.Replica{}, err @@ -7170,6 +7225,25 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat return metadata, nil } +func (q *FakeQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + keys := make([]database.ListProvisionerKeysByOrganizationRow, 0) + for _, key := range q.provisionerKeys { + if key.OrganizationID == organizationID { + keys = append(keys, database.ListProvisionerKeysByOrganizationRow{ + ID: key.ID, + CreatedAt: key.CreatedAt, + OrganizationID: key.OrganizationID, + Name: key.Name, + }) + } + } + + return keys, nil +} + func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index e705deafaf315..94306c2542bd6 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -326,6 +326,13 @@ func (m metricsStore) DeleteOrganizationMember(ctx context.Context, arg database return r0 } +func (m metricsStore) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error { + start := time.Now() + r0 := m.s.DeleteProvisionerKey(ctx, id) + m.queryLatencies.WithLabelValues("DeleteProvisionerKey").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { start := time.Now() err := m.s.DeleteReplicasUpdatedBefore(ctx, updatedAt) @@ -900,6 +907,13 @@ func (m metricsStore) GetProvisionerJobsCreatedAfter(ctx context.Context, create return jobs, err } +func (m metricsStore) GetProvisionerKeyByName(ctx context.Context, name database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { + start := time.Now() + r0, r1 := m.s.GetProvisionerKeyByName(ctx, name) + m.queryLatencies.WithLabelValues("GetProvisionerKeyByName").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetProvisionerLogsAfterID(ctx context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { start := time.Now() logs, err := m.s.GetProvisionerLogsAfterID(ctx, arg) @@ -1642,6 +1656,13 @@ func (m metricsStore) InsertProvisionerJobLogs(ctx context.Context, arg database return logs, err } +func (m metricsStore) InsertProvisionerKey(ctx context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { + start := time.Now() + r0, r1 := m.s.InsertProvisionerKey(ctx, arg) + m.queryLatencies.WithLabelValues("InsertProvisionerKey").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) { start := time.Now() replica, err := m.s.InsertReplica(ctx, arg) @@ -1803,6 +1824,13 @@ func (m metricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, arg d return metadata, err } +func (m metricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { + start := time.Now() + r0, r1 := m.s.ListProvisionerKeysByOrganization(ctx, organizationID) + m.queryLatencies.WithLabelValues("ListProvisionerKeysByOrganization").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { start := time.Now() r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index b69d982e46cc6..a9ba39e210e7a 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -542,6 +542,20 @@ func (mr *MockStoreMockRecorder) DeleteOrganizationMember(arg0, arg1 any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganizationMember", reflect.TypeOf((*MockStore)(nil).DeleteOrganizationMember), arg0, arg1) } +// DeleteProvisionerKey mocks base method. +func (m *MockStore) DeleteProvisionerKey(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteProvisionerKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteProvisionerKey indicates an expected call of DeleteProvisionerKey. +func (mr *MockStoreMockRecorder) DeleteProvisionerKey(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProvisionerKey", reflect.TypeOf((*MockStore)(nil).DeleteProvisionerKey), arg0, arg1) +} + // DeleteReplicasUpdatedBefore mocks base method. func (m *MockStore) DeleteReplicasUpdatedBefore(arg0 context.Context, arg1 time.Time) error { m.ctrl.T.Helper() @@ -1811,6 +1825,21 @@ func (mr *MockStoreMockRecorder) GetProvisionerJobsCreatedAfter(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsCreatedAfter), arg0, arg1) } +// GetProvisionerKeyByName mocks base method. +func (m *MockStore) GetProvisionerKeyByName(arg0 context.Context, arg1 database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerKeyByName", arg0, arg1) + ret0, _ := ret[0].(database.ProvisionerKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerKeyByName indicates an expected call of GetProvisionerKeyByName. +func (mr *MockStoreMockRecorder) GetProvisionerKeyByName(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerKeyByName", reflect.TypeOf((*MockStore)(nil).GetProvisionerKeyByName), arg0, arg1) +} + // GetProvisionerLogsAfterID mocks base method. func (m *MockStore) GetProvisionerLogsAfterID(arg0 context.Context, arg1 database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { m.ctrl.T.Helper() @@ -3441,6 +3470,21 @@ func (mr *MockStoreMockRecorder) InsertProvisionerJobLogs(arg0, arg1 any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertProvisionerJobLogs", reflect.TypeOf((*MockStore)(nil).InsertProvisionerJobLogs), arg0, arg1) } +// InsertProvisionerKey mocks base method. +func (m *MockStore) InsertProvisionerKey(arg0 context.Context, arg1 database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertProvisionerKey", arg0, arg1) + ret0, _ := ret[0].(database.ProvisionerKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertProvisionerKey indicates an expected call of InsertProvisionerKey. +func (mr *MockStoreMockRecorder) InsertProvisionerKey(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertProvisionerKey", reflect.TypeOf((*MockStore)(nil).InsertProvisionerKey), arg0, arg1) +} + // InsertReplica mocks base method. func (m *MockStore) InsertReplica(arg0 context.Context, arg1 database.InsertReplicaParams) (database.Replica, error) { m.ctrl.T.Helper() @@ -3778,6 +3822,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), arg0, arg1) } +// ListProvisionerKeysByOrganization mocks base method. +func (m *MockStore) ListProvisionerKeysByOrganization(arg0 context.Context, arg1 uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListProvisionerKeysByOrganization", arg0, arg1) + ret0, _ := ret[0].([]database.ListProvisionerKeysByOrganizationRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListProvisionerKeysByOrganization indicates an expected call of ListProvisionerKeysByOrganization. +func (mr *MockStoreMockRecorder) ListProvisionerKeysByOrganization(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProvisionerKeysByOrganization", reflect.TypeOf((*MockStore)(nil).ListProvisionerKeysByOrganization), arg0, arg1) +} + // ListWorkspaceAgentPortShares mocks base method. func (m *MockStore) ListWorkspaceAgentPortShares(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 4f2d21e19b37e..52d4519c99dfc 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -749,6 +749,14 @@ END) STORED NOT NULL COMMENT ON COLUMN provisioner_jobs.job_status IS 'Computed column to track the status of the job.'; +CREATE TABLE provisioner_keys ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + organization_id uuid NOT NULL, + name character varying(64) NOT NULL, + hashed_secret bytea NOT NULL +); + CREATE TABLE replicas ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -1584,6 +1592,12 @@ ALTER TABLE ONLY provisioner_job_logs ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); +ALTER TABLE ONLY provisioner_keys + ADD CONSTRAINT provisioner_keys_organization_id_name_key UNIQUE (organization_id, name); + +ALTER TABLE ONLY provisioner_keys + ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); + ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); @@ -1867,6 +1881,9 @@ ALTER TABLE ONLY provisioner_job_logs ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; +ALTER TABLE ONLY provisioner_keys + ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 3a9557a9758dd..6e6eef8862b72 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -28,6 +28,7 @@ const ( ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; + ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyTailnetAgentsCoordinatorID ForeignKeyConstraint = "tailnet_agents_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetClientSubscriptionsCoordinatorID ForeignKeyConstraint = "tailnet_client_subscriptions_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetClientsCoordinatorID ForeignKeyConstraint = "tailnet_clients_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_clients ADD CONSTRAINT tailnet_clients_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000223_provisioner_keys.down.sql b/coderd/database/migrations/000223_provisioner_keys.down.sql new file mode 100644 index 0000000000000..264b235facff2 --- /dev/null +++ b/coderd/database/migrations/000223_provisioner_keys.down.sql @@ -0,0 +1 @@ +DROP TABLE provisioner_keys; diff --git a/coderd/database/migrations/000223_provisioner_keys.up.sql b/coderd/database/migrations/000223_provisioner_keys.up.sql new file mode 100644 index 0000000000000..39e327a5341d7 --- /dev/null +++ b/coderd/database/migrations/000223_provisioner_keys.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE provisioner_keys ( + id uuid PRIMARY KEY, + created_at timestamptz NOT NULL, + organization_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, + name varchar(64) NOT NULL, + hashed_secret bytea NOT NULL, + UNIQUE (organization_id, name) +); diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index f8a3fc2c537b1..85d08cbfba8ec 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -212,6 +212,12 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object { return rbac.ResourceProvisionerDaemon.WithID(p.ID) } +func (p ProvisionerKey) RBACObject() rbac.Object { + return rbac.ResourceProvisionerKeys. + WithID(p.ID). + InOrg(p.OrganizationID) +} + func (w WorkspaceProxy) RBACObject() rbac.Object { return rbac.ResourceWorkspaceProxy. WithID(w.ID) diff --git a/coderd/database/models.go b/coderd/database/models.go index 8ace22909db93..9b35e1c0f79b3 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2185,6 +2185,14 @@ type ProvisionerJobLog struct { ID int64 `db:"id" json:"id"` } +type ProvisionerKey struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Name string `db:"name" json:"name"` + HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"` +} + type Replica struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 917db96b207d1..15e14ff6334f7 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -92,6 +92,7 @@ type sqlcQuerier interface { DeleteOldWorkspaceAgentStats(ctx context.Context) error DeleteOrganization(ctx context.Context, id uuid.UUID) error DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error + DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error) @@ -184,6 +185,7 @@ type sqlcQuerier interface { GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) + GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) @@ -346,6 +348,7 @@ type sqlcQuerier interface { InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) + InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error @@ -370,6 +373,7 @@ type sqlcQuerier interface { InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) + ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ListProvisionerKeysByOrganizationRow, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) // Arguments are optional with uuid.Nil to ignore. // - Use just 'organization_id' to get all members of an org diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ee439996e34dd..2c9d9e6cbad37 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5455,6 +5455,134 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a return err } +const deleteProvisionerKey = `-- name: DeleteProvisionerKey :exec +DELETE FROM + provisioner_keys +WHERE + id = $1 +` + +func (q *sqlQuerier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteProvisionerKey, id) + return err +} + +const getProvisionerKeyByName = `-- name: GetProvisionerKeyByName :one +SELECT + id, created_at, organization_id, name, hashed_secret +FROM + provisioner_keys +WHERE + organization_id = $1 +AND + name = $2 +` + +type GetProvisionerKeyByNameParams struct { + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) { + row := q.db.QueryRowContext(ctx, getProvisionerKeyByName, arg.OrganizationID, arg.Name) + var i ProvisionerKey + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.OrganizationID, + &i.Name, + &i.HashedSecret, + ) + return i, err +} + +const insertProvisionerKey = `-- name: InsertProvisionerKey :one +INSERT INTO + provisioner_keys ( + id, + created_at, + organization_id, + name, + hashed_secret + ) +VALUES + ($1, $2, $3, $4, $5) RETURNING id, created_at, organization_id, name, hashed_secret +` + +type InsertProvisionerKeyParams struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Name string `db:"name" json:"name"` + HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"` +} + +func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) { + row := q.db.QueryRowContext(ctx, insertProvisionerKey, + arg.ID, + arg.CreatedAt, + arg.OrganizationID, + arg.Name, + arg.HashedSecret, + ) + var i ProvisionerKey + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.OrganizationID, + &i.Name, + &i.HashedSecret, + ) + return i, err +} + +const listProvisionerKeysByOrganization = `-- name: ListProvisionerKeysByOrganization :many +SELECT + id, + created_at, + organization_id, + name +FROM + provisioner_keys +WHERE + organization_id = $1 +` + +type ListProvisionerKeysByOrganizationRow struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ListProvisionerKeysByOrganizationRow, error) { + rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganization, organizationID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListProvisionerKeysByOrganizationRow + for rows.Next() { + var i ListProvisionerKeysByOrganizationRow + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.OrganizationID, + &i.Name, + ); 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 getWorkspaceProxies = `-- name: GetWorkspaceProxies :many SELECT id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version diff --git a/coderd/database/queries/provisionerkeys.sql b/coderd/database/queries/provisionerkeys.sql new file mode 100644 index 0000000000000..19c203ac9ca7d --- /dev/null +++ b/coderd/database/queries/provisionerkeys.sql @@ -0,0 +1,38 @@ +-- name: InsertProvisionerKey :one +INSERT INTO + provisioner_keys ( + id, + created_at, + organization_id, + name, + hashed_secret + ) +VALUES + ($1, $2, $3, $4, $5) RETURNING *; + +-- name: GetProvisionerKeyByName :one +SELECT + * +FROM + provisioner_keys +WHERE + organization_id = $1 +AND + name = $2; + +-- name: ListProvisionerKeysByOrganization :many +SELECT + id, + created_at, + organization_id, + name +FROM + provisioner_keys +WHERE + organization_id = $1; + +-- name: DeleteProvisionerKey :exec +DELETE FROM + provisioner_keys +WHERE + id = $1; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index d090af80626b8..6702e862e5378 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -44,6 +44,8 @@ const ( UniqueProvisionerDaemonsPkey UniqueConstraint = "provisioner_daemons_pkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_pkey PRIMARY KEY (id); UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); + UniqueProvisionerKeysOrganizationIDNameKey UniqueConstraint = "provisioner_keys_organization_id_name_key" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_name_key UNIQUE (organization_id, name); + UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetClientSubscriptionsPkey UniqueConstraint = "tailnet_client_subscriptions_pkey" // ALTER TABLE ONLY tailnet_client_subscriptions ADD CONSTRAINT tailnet_client_subscriptions_pkey PRIMARY KEY (client_id, coordinator_id, agent_id); diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 5b39b846195dd..bc2846da49564 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -161,6 +161,15 @@ var ( Type: "provisioner_daemon", } + // ResourceProvisionerKeys + // Valid Actions + // - "ActionCreate" :: create a provisioner key + // - "ActionDelete" :: delete a provisioner key + // - "ActionRead" :: read provisioner keys + ResourceProvisionerKeys = Object{ + Type: "provisioner_keys", + } + // ResourceReplicas // Valid Actions // - "ActionRead" :: read replicas @@ -269,6 +278,7 @@ func AllResources() []Objecter { ResourceOrganization, ResourceOrganizationMember, ResourceProvisionerDaemon, + ResourceProvisionerKeys, ResourceReplicas, ResourceSystem, ResourceTailnetCoordinator, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index eec8865d09317..1fe635bec5e61 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -160,6 +160,13 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionDelete: actDef("delete a provisioner daemon"), }, }, + "provisioner_keys": { + Actions: map[Action]ActionDefinition{ + ActionCreate: actDef("create a provisioner key"), + ActionRead: actDef("read provisioner keys"), + ActionDelete: actDef("delete a provisioner key"), + }, + }, "organization": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef("create an organization"), diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 73d784b449535..573fea66b8c80 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -21,6 +21,7 @@ const ( ResourceOrganization RBACResource = "organization" ResourceOrganizationMember RBACResource = "organization_member" ResourceProvisionerDaemon RBACResource = "provisioner_daemon" + ResourceProvisionerKeys RBACResource = "provisioner_keys" ResourceReplicas RBACResource = "replicas" ResourceSystem RBACResource = "system" ResourceTailnetCoordinator RBACResource = "tailnet_coordinator" @@ -69,6 +70,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, + ResourceProvisionerKeys: {ActionCreate, ActionDelete, ActionRead}, ResourceReplicas: {ActionRead}, ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, diff --git a/docs/api/members.md b/docs/api/members.md index 1a9beae285157..d8dfc6499ee65 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -182,6 +182,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | | `resource_type` | `tailnet_coordinator` | @@ -304,6 +305,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | | `resource_type` | `tailnet_coordinator` | @@ -578,6 +580,7 @@ Status Code **200** | `resource_type` | `organization` | | `resource_type` | `organization_member` | | `resource_type` | `provisioner_daemon` | +| `resource_type` | `provisioner_keys` | | `resource_type` | `replicas` | | `resource_type` | `system` | | `resource_type` | `tailnet_coordinator` | diff --git a/docs/api/schemas.md b/docs/api/schemas.md index a3e745b46fa17..19687dd55db8a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3958,6 +3958,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `organization` | | `organization_member` | | `provisioner_daemon` | +| `provisioner_keys` | | `replicas` | | `system` | | `tailnet_coordinator` | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d4bbc32bba10c..23048351e4101 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2206,6 +2206,7 @@ export type RBACResource = | "organization" | "organization_member" | "provisioner_daemon" + | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" @@ -2232,6 +2233,7 @@ export const RBACResources: RBACResource[] = [ "organization", "organization_member", "provisioner_daemon", + "provisioner_keys", "replicas", "system", "tailnet_coordinator", From 2d2ea402ad63b8d62f8a6a909744aedc4dea588b Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 9 Jul 2024 20:02:11 +0000 Subject: [PATCH 02/20] add api routes --- coderd/coderd.go | 10 +++ coderd/httpmw/provisionerkey.go | 58 ++++++++++++++++ coderd/provisionerkey/provisionerkey.go | 31 +++++++++ coderd/provisionerkeys.go | 92 +++++++++++++++++++++++++ codersdk/provisionerdaemons.go | 68 ++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 coderd/httpmw/provisionerkey.go create mode 100644 coderd/provisionerkey/provisionerkey.go create mode 100644 coderd/provisionerkeys.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 0a3414fdb984c..199c88945c856 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -915,6 +915,16 @@ func New(options *Options) *API { }) }) }) + r.Route("/provisionerkeys", func(r chi.Router) { + r.Get("/", api.provisionerKeys) + r.Post("/", api.postProvisionerKey) + r.Route("/{provisionerKey}", func(r chi.Router) { + r.Use( + httpmw.ExtractProvisionerKeyParam(options.Database), + ) + r.Delete("/", api.deleteProvisionerKey) + }) + }) }) }) r.Route("/templates", func(r chi.Router) { diff --git a/coderd/httpmw/provisionerkey.go b/coderd/httpmw/provisionerkey.go new file mode 100644 index 0000000000000..adaf2088479f2 --- /dev/null +++ b/coderd/httpmw/provisionerkey.go @@ -0,0 +1,58 @@ +package httpmw + +import ( + "context" + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" +) + +type provisionerKeyParamContextKey struct{} + +// ProvisionerKeyParam returns the user from the ExtractProvisionerKeyParam handler. +func ProvisionerKeyParam(r *http.Request) database.ProvisionerKey { + user, ok := r.Context().Value(userParamContextKey{}).(database.ProvisionerKey) + if !ok { + panic("developer error: provisioner key parameter middleware not provided") + } + return user +} + +// ExtractProvisionerKeyParam extracts a provisioner key from a name in the {provisionerKey} URL +// parameter. +func ExtractProvisionerKeyParam(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + organization := OrganizationParam(r) + + provisionerKeyQuery := chi.URLParam(r, "provisionerKey") + if provisionerKeyQuery == "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "\"provisionerKey\" must be provided.", + }) + return + } + + provisionerKey, err := db.GetProvisionerKeyByName(ctx, database.GetProvisionerKeyByNameParams{ + OrganizationID: organization.ID, + Name: provisionerKeyQuery, + }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + ctx = context.WithValue(ctx, provisionerKeyParamContextKey{}, provisionerKey) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/coderd/provisionerkey/provisionerkey.go b/coderd/provisionerkey/provisionerkey.go new file mode 100644 index 0000000000000..4df23125be2d3 --- /dev/null +++ b/coderd/provisionerkey/provisionerkey.go @@ -0,0 +1,31 @@ +package provisionerkey + +import ( + "crypto/sha256" + "fmt" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/cryptorand" +) + +func New(organizationID uuid.UUID, name string) (database.InsertProvisionerKeyParams, string, error) { + id := uuid.New() + secret, err := cryptorand.HexString(64) + if err != nil { + return database.InsertProvisionerKeyParams{}, "", xerrors.Errorf("generate token: %w", err) + } + hashedSecret := sha256.Sum256([]byte(secret)) + token := fmt.Sprintf("%s:%s", id, secret) + + return database.InsertProvisionerKeyParams{ + ID: id, + CreatedAt: dbtime.Now(), + OrganizationID: organizationID, + Name: name, + HashedSecret: hashedSecret[:], + }, token, nil +} diff --git a/coderd/provisionerkeys.go b/coderd/provisionerkeys.go new file mode 100644 index 0000000000000..53b6f5f7a0700 --- /dev/null +++ b/coderd/provisionerkeys.go @@ -0,0 +1,92 @@ +package coderd + +import ( + "database/sql" + "errors" + "net/http" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/provisionerkey" + "github.com/coder/coder/v2/codersdk" +) + +func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + organization := httpmw.OrganizationParam(r) + + var req codersdk.CreateProvisionerKeyRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + params, token, err := provisionerkey.New(organization.ID, req.Name) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + _, err = api.Database.InsertProvisionerKey(ctx, params) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateProvisionerKeyResponse{ + Key: token, + }) +} + +func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + organization := httpmw.OrganizationParam(r) + + pks, err := api.Database.ListProvisionerKeysByOrganization(ctx, organization.ID) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerKeys(pks)) +} + +func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + organization := httpmw.OrganizationParam(r) + provisionerKey := httpmw.ProvisionerKeyParam(r) + + pk, err := api.Database.GetProvisionerKeyByName(ctx, database.GetProvisionerKeyByNameParams{ + OrganizationID: organization.ID, + Name: provisionerKey.Name, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + httpapi.ResourceNotFound(rw) + return + } + httpapi.InternalServerError(rw, err) + return + } + + err = api.Database.DeleteProvisionerKey(ctx, pk.ID) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + httpapi.Write(ctx, rw, http.StatusNoContent, nil) +} + +func convertProvisionerKeys(dbKeys []database.ListProvisionerKeysByOrganizationRow) []codersdk.ProvisionerKey { + keys := make([]codersdk.ProvisionerKey, 0, len(dbKeys)) + for _, dbKey := range dbKeys { + keys = append(keys, codersdk.ProvisionerKey{ + ID: dbKey.ID, + CreatedAt: dbKey.CreatedAt, + OrganizationID: dbKey.OrganizationID, + Name: dbKey.Name, + }) + } + return keys +} diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index e26778f940045..f3355b703a4a6 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -265,3 +265,71 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione } return proto.NewDRPCProvisionerDaemonClient(drpc.MultiplexedConn(session)), nil } + +type ProvisionerKey struct { + ID uuid.UUID `json:"id" format:"uuid"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + OrganizationID uuid.UUID `json:"organization" format:"uuid"` + Name string `json:"name"` +} + +type CreateProvisionerKeyRequest struct { + Name string `json:"name"` +} + +type CreateProvisionerKeyResponse struct { + Key string +} + +// CreateProvisionerKey creates a new provisioner key for an organization. +func (c *Client) CreateProvisionerKey(ctx context.Context, organizationID uuid.UUID, req CreateProvisionerKeyRequest) (CreateProvisionerKeyResponse, error) { + res, err := c.Request(ctx, http.MethodPost, + fmt.Sprintf("/api/v2/organizations/%s/provisionerkeys", organizationID.String()), + req, + ) + if err != nil { + return CreateProvisionerKeyResponse{}, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusCreated { + return CreateProvisionerKeyResponse{}, ReadBodyAsError(res) + } + var resp CreateProvisionerKeyResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// ListProvisionerKeys lists all provisioner keys for an organization. +func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) { + res, err := c.Request(ctx, http.MethodPost, + fmt.Sprintf("/api/v2/organizations/%s/provisionerkeys", organizationID.String()), + nil, + ) + if err != nil { + return nil, xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusCreated { + return nil, ReadBodyAsError(res) + } + var resp []ProvisionerKey + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// DeleteProvisionerKey deletes a provisioner key. +func (c *Client) DeleteProvisionerKey(ctx context.Context, organizationID uuid.UUID, name string) error { + res, err := c.Request(ctx, http.MethodDelete, + fmt.Sprintf("/api/v2/organizations/%s/provisionerkeys/%s", organizationID.String(), name), + nil, + ) + if err != nil { + return xerrors.Errorf("make request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} From ea9fa642d90c810f2f5ea36f56edc9d5416c8584 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 11 Jul 2024 17:33:11 +0000 Subject: [PATCH 03/20] testing --- coderd/provisionerkeys_test.go | 87 ++++++++++++++++++++++++++++++++++ codersdk/provisionerdaemons.go | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 coderd/provisionerkeys_test.go diff --git a/coderd/provisionerkeys_test.go b/coderd/provisionerkeys_test.go new file mode 100644 index 0000000000000..60f082ab9a678 --- /dev/null +++ b/coderd/provisionerkeys_test.go @@ -0,0 +1,87 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" +) + +func TestProvisionerKeys(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + t.Cleanup(cancel) + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) + member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "other", + }) + require.NoError(t, err, "create org") + outsideOrgAdmin, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.ScopedRoleOrgAdmin(otherOrg.ID)) + + // member cannot create a provisioner key + _, err = member.CreateProvisionerKey(ctx, otherOrg.ID, codersdk.CreateProvisionerKeyRequest{ + Name: "key", + }) + require.ErrorContains(t, err, "Resource not found") + + // member cannot list provisioner keys + _, err = member.ListProvisionerKeys(ctx, otherOrg.ID) + require.ErrorContains(t, err, "Resource not found") + + // member cannot delete a provisioner key + err = member.DeleteProvisionerKey(ctx, otherOrg.ID, "key") + require.ErrorContains(t, err, "Resource not found") + + // outside org admin cannot create a provisioner key + _, err = outsideOrgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ + Name: "key", + }) + require.ErrorContains(t, err, "Resource not found") + + // outside org admin cannot list provisioner keys + _, err = outsideOrgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID) + require.ErrorContains(t, err, "Resource not found") + + // outside org admin cannot delete a provisioner key + err = outsideOrgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") + require.ErrorContains(t, err, "Resource not found") + + // org admin can list provisioner keys and get an empty list + keys, err := orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID) + require.NoError(t, err, "org admin list provisioner keys") + require.Len(t, keys, 0, "org admin list provisioner keys") + + // org admin can create a provisioner key + _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ + Name: "key", + }) + require.NoError(t, err, "org admin create provisioner key") + + // org admin can conflict on name creating a provisioner key + _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ + Name: "key", + }) + require.Error(t, err, "org admin create provisioner key") + + // org admin can list provisioner keys + keys, err = orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID) + require.NoError(t, err, "org admin list provisioner keys") + require.Len(t, keys, 1, "org admin list provisioner keys") + + // org admin can delete a provisioner key + err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") + require.NoError(t, err, "org admin delete provisioner key") + + // org admin cannot delete a provisioner key that doesn't exist + err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") + require.Error(t, err, "org admin delete provisioner key") +} diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index f3355b703a4a6..fa0ad49db6908 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -301,7 +301,7 @@ func (c *Client) CreateProvisionerKey(ctx context.Context, organizationID uuid.U // ListProvisionerKeys lists all provisioner keys for an organization. func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) { - res, err := c.Request(ctx, http.MethodPost, + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/provisionerkeys", organizationID.String()), nil, ) From 5ce5549153b97a133d8fb63834e5b99ed73c5169 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 11 Jul 2024 19:26:33 +0000 Subject: [PATCH 04/20] more tests and rbac fix --- coderd/coderd.go | 2 +- coderd/database/dbauthz/dbauthz.go | 24 +++++++-------------- coderd/database/dbmem/dbmem.go | 13 +++++++++++ coderd/database/dbmetrics/dbmetrics.go | 7 ++++++ coderd/database/dbmock/dbmock.go | 15 +++++++++++++ coderd/database/modelmethods.go | 6 ++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 22 +++++++++++++++++++ coderd/database/queries/provisionerkeys.sql | 8 +++++++ coderd/httpmw/provisionerkey.go | 6 +++--- coderd/provisionerkeys_test.go | 24 ++++++++++++++++++--- codersdk/provisionerdaemons.go | 2 +- site/src/api/typesGenerated.ts | 18 ++++++++++++++++ 13 files changed, 124 insertions(+), 24 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 199c88945c856..56df04c14eb9e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -918,7 +918,7 @@ func New(options *Options) *API { r.Route("/provisionerkeys", func(r chi.Router) { r.Get("/", api.provisionerKeys) r.Post("/", api.postProvisionerKey) - r.Route("/{provisionerKey}", func(r chi.Router) { + r.Route("/{provisionerkey}", func(r chi.Router) { r.Use( httpmw.ExtractProvisionerKeyParam(options.Database), ) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 3abb9fc6ce405..7f48b7e5e5687 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1075,10 +1075,7 @@ func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.Del } func (q *querier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceProvisionerKeys); err != nil { - return err - } - return q.db.DeleteProvisionerKey(ctx, id) + return deleteQ(q.log, q.auth, q.db.GetProvisionerKeyByID, q.db.DeleteProvisionerKey)(ctx, id) } func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { @@ -1678,11 +1675,12 @@ func (q *querier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt return q.db.GetProvisionerJobsCreatedAfter(ctx, createdAt) } +func (q *querier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (database.ProvisionerKey, error) { + return fetch(q.log, q.auth, q.db.GetProvisionerKeyByID)(ctx, id) +} + func (q *querier) GetProvisionerKeyByName(ctx context.Context, name database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerKeys); err != nil { - return database.ProvisionerKey{}, err - } - return q.db.GetProvisionerKeyByName(ctx, name) + return fetch(q.log, q.auth, q.db.GetProvisionerKeyByName)(ctx, name) } func (q *querier) GetProvisionerLogsAfterID(ctx context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { @@ -2630,10 +2628,7 @@ func (q *querier) InsertProvisionerJobLogs(ctx context.Context, arg database.Ins } func (q *querier) InsertProvisionerKey(ctx context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceProvisionerKeys); err != nil { - return database.ProvisionerKey{}, err - } - return q.db.InsertProvisionerKey(ctx, arg) + return insert(q.log, q.auth, rbac.ResourceProvisionerKeys.InOrg(arg.OrganizationID), q.db.InsertProvisionerKey)(ctx, arg) } func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) { @@ -2865,10 +2860,7 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab } func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerKeys); err != nil { - return nil, err - } - return q.db.ListProvisionerKeysByOrganization(ctx, organizationID) + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID) } func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 32d47d5124963..cc67ebc4732e5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3210,6 +3210,19 @@ func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after ti return jobs, nil } +func (q *FakeQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (database.ProvisionerKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, key := range q.provisionerKeys { + if key.ID == id { + return key, nil + } + } + + return database.ProvisionerKey{}, sql.ErrNoRows +} + func (q *FakeQuerier) GetProvisionerKeyByName(ctx context.Context, arg database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 94306c2542bd6..a46cc3ee5cdf7 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -907,6 +907,13 @@ func (m metricsStore) GetProvisionerJobsCreatedAfter(ctx context.Context, create return jobs, err } +func (m metricsStore) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (database.ProvisionerKey, error) { + start := time.Now() + r0, r1 := m.s.GetProvisionerKeyByID(ctx, id) + m.queryLatencies.WithLabelValues("GetProvisionerKeyByID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetProvisionerKeyByName(ctx context.Context, name database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { start := time.Now() r0, r1 := m.s.GetProvisionerKeyByName(ctx, name) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index a9ba39e210e7a..83a348c66abd5 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1825,6 +1825,21 @@ func (mr *MockStoreMockRecorder) GetProvisionerJobsCreatedAfter(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsCreatedAfter), arg0, arg1) } +// GetProvisionerKeyByID mocks base method. +func (m *MockStore) GetProvisionerKeyByID(arg0 context.Context, arg1 uuid.UUID) (database.ProvisionerKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerKeyByID", arg0, arg1) + ret0, _ := ret[0].(database.ProvisionerKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerKeyByID indicates an expected call of GetProvisionerKeyByID. +func (mr *MockStoreMockRecorder) GetProvisionerKeyByID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerKeyByID", reflect.TypeOf((*MockStore)(nil).GetProvisionerKeyByID), arg0, arg1) +} + // GetProvisionerKeyByName mocks base method. func (m *MockStore) GetProvisionerKeyByName(arg0 context.Context, arg1 database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { m.ctrl.T.Helper() diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 85d08cbfba8ec..dbf06518500ff 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -218,6 +218,12 @@ func (p ProvisionerKey) RBACObject() rbac.Object { InOrg(p.OrganizationID) } +func (p ListProvisionerKeysByOrganizationRow) RBACObject() rbac.Object { + return rbac.ResourceProvisionerKeys. + WithID(p.ID). + InOrg(p.OrganizationID) +} + func (w WorkspaceProxy) RBACObject() rbac.Object { return rbac.ResourceWorkspaceProxy. WithID(w.ID) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 15e14ff6334f7..c7e003f16b361 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -185,6 +185,7 @@ type sqlcQuerier interface { GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) + GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2c9d9e6cbad37..28c257d2e0bd7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5467,6 +5467,28 @@ func (q *sqlQuerier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) err return err } +const getProvisionerKeyByID = `-- name: GetProvisionerKeyByID :one +SELECT + id, created_at, organization_id, name, hashed_secret +FROM + provisioner_keys +WHERE + id = $1 +` + +func (q *sqlQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) { + row := q.db.QueryRowContext(ctx, getProvisionerKeyByID, id) + var i ProvisionerKey + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.OrganizationID, + &i.Name, + &i.HashedSecret, + ) + return i, err +} + const getProvisionerKeyByName = `-- name: GetProvisionerKeyByName :one SELECT id, created_at, organization_id, name, hashed_secret diff --git a/coderd/database/queries/provisionerkeys.sql b/coderd/database/queries/provisionerkeys.sql index 19c203ac9ca7d..5a6eea50ca159 100644 --- a/coderd/database/queries/provisionerkeys.sql +++ b/coderd/database/queries/provisionerkeys.sql @@ -10,6 +10,14 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5) RETURNING *; +-- name: GetProvisionerKeyByID :one +SELECT + * +FROM + provisioner_keys +WHERE + id = $1; + -- name: GetProvisionerKeyByName :one SELECT * diff --git a/coderd/httpmw/provisionerkey.go b/coderd/httpmw/provisionerkey.go index adaf2088479f2..484200f469422 100644 --- a/coderd/httpmw/provisionerkey.go +++ b/coderd/httpmw/provisionerkey.go @@ -15,7 +15,7 @@ type provisionerKeyParamContextKey struct{} // ProvisionerKeyParam returns the user from the ExtractProvisionerKeyParam handler. func ProvisionerKeyParam(r *http.Request) database.ProvisionerKey { - user, ok := r.Context().Value(userParamContextKey{}).(database.ProvisionerKey) + user, ok := r.Context().Value(provisionerKeyParamContextKey{}).(database.ProvisionerKey) if !ok { panic("developer error: provisioner key parameter middleware not provided") } @@ -30,10 +30,10 @@ func ExtractProvisionerKeyParam(db database.Store) func(http.Handler) http.Handl ctx := r.Context() organization := OrganizationParam(r) - provisionerKeyQuery := chi.URLParam(r, "provisionerKey") + provisionerKeyQuery := chi.URLParam(r, "provisionerkey") if provisionerKeyQuery == "" { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "\"provisionerKey\" must be provided.", + Message: "\"provisionerkey\" must be provided.", }) return } diff --git a/coderd/provisionerkeys_test.go b/coderd/provisionerkeys_test.go index 60f082ab9a678..9dc8517417854 100644 --- a/coderd/provisionerkeys_test.go +++ b/coderd/provisionerkeys_test.go @@ -15,7 +15,7 @@ import ( func TestProvisionerKeys(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*10) t.Cleanup(cancel) client := coderdtest.New(t, nil) owner := coderdtest.CreateFirstUser(t, client) @@ -70,7 +70,25 @@ func TestProvisionerKeys(t *testing.T) { _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ Name: "key", }) - require.Error(t, err, "org admin create provisioner key") + require.ErrorContains(t, err, "already exists") + + // key name cannot have special characters + _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ + Name: "key with spaces", + }) + require.ErrorContains(t, err, "org admin create provisioner key") + + // key name cannot be too long + _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ + Name: "key with spaces", + }) + require.ErrorContains(t, err, "less than 64 characters") + + // key name cannot be empty + _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ + Name: "", + }) + require.ErrorContains(t, err, "cannot be empty") // org admin can list provisioner keys keys, err = orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID) @@ -83,5 +101,5 @@ func TestProvisionerKeys(t *testing.T) { // org admin cannot delete a provisioner key that doesn't exist err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") - require.Error(t, err, "org admin delete provisioner key") + require.ErrorContains(t, err, "Resource not found") } diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index fa0ad49db6908..77dc22c0c5ebd 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -310,7 +310,7 @@ func (c *Client) ListProvisionerKeys(ctx context.Context, organizationID uuid.UU } defer res.Body.Close() - if res.StatusCode != http.StatusCreated { + if res.StatusCode != http.StatusOK { return nil, ReadBodyAsError(res) } var resp []ProvisionerKey diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 23048351e4101..54d48bd1d1971 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -233,6 +233,16 @@ export interface CreateOrganizationRequest { readonly icon?: string; } +// From codersdk/provisionerdaemons.go +export interface CreateProvisionerKeyRequest { + readonly name: string; +} + +// From codersdk/provisionerdaemons.go +export interface CreateProvisionerKeyResponse { + readonly Key: string; +} + // From codersdk/organizations.go export interface CreateTemplateRequest { readonly name: string; @@ -955,6 +965,14 @@ export interface ProvisionerJobLog { readonly output: string; } +// From codersdk/provisionerdaemons.go +export interface ProvisionerKey { + readonly id: string; + readonly created_at: string; + readonly organization: string; + readonly name: string; +} + // From codersdk/workspaceproxy.go export interface ProxyHealthReport { readonly errors: readonly string[]; From 06f2e94765cc61cc6702da662d5e911f698017c1 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 16:09:58 +0000 Subject: [PATCH 05/20] fix file --- coderd/notifications/manager_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/coderd/notifications/manager_test.go b/coderd/notifications/manager_test.go index 93ba158b48a65..cabcc84bcf32f 100644 --- a/coderd/notifications/manager_test.go +++ b/coderd/notifications/manager_test.go @@ -7,12 +7,18 @@ import ( "testing" "time" + "github.com/coder/serpent" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/xerrors" +<<<<<<< HEAD "github.com/coder/serpent" +======= + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" +>>>>>>> dadd7609f (fix file) "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" From 240861221a39b6d13597354dd597ea0f16efe9d2 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 16:49:22 +0000 Subject: [PATCH 06/20] tests --- coderd/database/dbmem/dbmem.go | 4 +++- coderd/provisionerkeys.go | 33 +++++++++++++++++++++++++++++++++ coderd/provisionerkeys_test.go | 14 ++++---------- codersdk/provisionerdaemons.go | 2 +- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index cc67ebc4732e5..ac259c16e80eb 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6543,9 +6543,11 @@ func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.Inser q.mutex.Lock() defer q.mutex.Unlock() + newErr := *errUniqueConstraint + newErr.Constraint = string(database.UniqueProvisionerKeysOrganizationIDNameKey) for _, key := range q.provisionerKeys { if key.ID == arg.ID || (key.OrganizationID == arg.OrganizationID && key.Name == arg.Name) { - return database.ProvisionerKey{}, errUniqueConstraint + return database.ProvisionerKey{}, &newErr } } diff --git a/coderd/provisionerkeys.go b/coderd/provisionerkeys.go index 53b6f5f7a0700..3091e8f4f4b9f 100644 --- a/coderd/provisionerkeys.go +++ b/coderd/provisionerkeys.go @@ -3,6 +3,7 @@ package coderd import ( "database/sql" "errors" + "fmt" "net/http" "github.com/coder/coder/v2/coderd/database" @@ -21,6 +22,32 @@ func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { return } + if req.Name == "" { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Name is required", + Validations: []codersdk.ValidationError{ + { + Field: "name", + Detail: "Name is required", + }, + }, + }) + return + } + + if len(req.Name) > 64 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Name must be at most 64 characters", + Validations: []codersdk.ValidationError{ + { + Field: "name", + Detail: "Name must be at most 64 characters", + }, + }, + }) + return + } + params, token, err := provisionerkey.New(organization.ID, req.Name) if err != nil { httpapi.InternalServerError(rw, err) @@ -28,6 +55,12 @@ func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { } _, err = api.Database.InsertProvisionerKey(ctx, params) + if database.IsUniqueViolation(err, database.UniqueProvisionerKeysOrganizationIDNameKey) { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: fmt.Sprintf("Provisioner key with name '%s' already exists in organization", req.Name), + }) + return + } if err != nil { httpapi.InternalServerError(rw, err) return diff --git a/coderd/provisionerkeys_test.go b/coderd/provisionerkeys_test.go index 9dc8517417854..f406114ec13ed 100644 --- a/coderd/provisionerkeys_test.go +++ b/coderd/provisionerkeys_test.go @@ -70,25 +70,19 @@ func TestProvisionerKeys(t *testing.T) { _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ Name: "key", }) - require.ErrorContains(t, err, "already exists") - - // key name cannot have special characters - _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ - Name: "key with spaces", - }) - require.ErrorContains(t, err, "org admin create provisioner key") + require.ErrorContains(t, err, "already exists in organization") // key name cannot be too long _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ - Name: "key with spaces", + Name: "Everyone please pass your watermelons to the front of the pool, the storm is approaching.", }) - require.ErrorContains(t, err, "less than 64 characters") + require.ErrorContains(t, err, "must be at most 64 characters") // key name cannot be empty _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ Name: "", }) - require.ErrorContains(t, err, "cannot be empty") + require.ErrorContains(t, err, "is required") // org admin can list provisioner keys keys, err = orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID) diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 77dc22c0c5ebd..0bd66cf3742d3 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -278,7 +278,7 @@ type CreateProvisionerKeyRequest struct { } type CreateProvisionerKeyResponse struct { - Key string + Key string `json:"key"` } // CreateProvisionerKey creates a new provisioner key for an organization. From 67701dee6f922b8aadc4a396c07125f4cb7d042a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 17:19:02 +0000 Subject: [PATCH 07/20] gen --- coderd/database/dbmem/dbmem.go | 6 +++--- site/src/api/typesGenerated.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ac259c16e80eb..25d791800ddd4 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3210,7 +3210,7 @@ func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after ti return jobs, nil } -func (q *FakeQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (database.ProvisionerKey, error) { +func (q *FakeQuerier) GetProvisionerKeyByID(_ context.Context, id uuid.UUID) (database.ProvisionerKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -3223,7 +3223,7 @@ func (q *FakeQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) ( return database.ProvisionerKey{}, sql.ErrNoRows } -func (q *FakeQuerier) GetProvisionerKeyByName(ctx context.Context, arg database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { +func (q *FakeQuerier) GetProvisionerKeyByName(_ context.Context, arg database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -7240,7 +7240,7 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat return metadata, nil } -func (q *FakeQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { +func (q *FakeQuerier) ListProvisionerKeysByOrganization(_ context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 54d48bd1d1971..aa6b471b5b54d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -240,7 +240,7 @@ export interface CreateProvisionerKeyRequest { // From codersdk/provisionerdaemons.go export interface CreateProvisionerKeyResponse { - readonly Key: string; + readonly key: string; } // From codersdk/organizations.go From 53dccb7b42742d8f40a35bfe97d2c4189e56eb5b Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 17:21:27 +0000 Subject: [PATCH 08/20] fix migrations --- ...ioner_keys.down.sql => 000226_provisioner_keys.down.sql} | 0 ...ovisioner_keys.up.sql => 000226_provisioner_keys.up.sql} | 0 coderd/notifications/manager_test.go | 6 ------ 3 files changed, 6 deletions(-) rename coderd/database/migrations/{000223_provisioner_keys.down.sql => 000226_provisioner_keys.down.sql} (100%) rename coderd/database/migrations/{000223_provisioner_keys.up.sql => 000226_provisioner_keys.up.sql} (100%) diff --git a/coderd/database/migrations/000223_provisioner_keys.down.sql b/coderd/database/migrations/000226_provisioner_keys.down.sql similarity index 100% rename from coderd/database/migrations/000223_provisioner_keys.down.sql rename to coderd/database/migrations/000226_provisioner_keys.down.sql diff --git a/coderd/database/migrations/000223_provisioner_keys.up.sql b/coderd/database/migrations/000226_provisioner_keys.up.sql similarity index 100% rename from coderd/database/migrations/000223_provisioner_keys.up.sql rename to coderd/database/migrations/000226_provisioner_keys.up.sql diff --git a/coderd/notifications/manager_test.go b/coderd/notifications/manager_test.go index cabcc84bcf32f..93ba158b48a65 100644 --- a/coderd/notifications/manager_test.go +++ b/coderd/notifications/manager_test.go @@ -7,18 +7,12 @@ import ( "testing" "time" - "github.com/coder/serpent" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/xerrors" -<<<<<<< HEAD "github.com/coder/serpent" -======= - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" ->>>>>>> dadd7609f (fix file) "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" From 5a7e2deef291de31d5f1502f07c09c1d104b06e9 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 17:42:34 +0000 Subject: [PATCH 09/20] add swagger --- coderd/provisionerkeys.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/coderd/provisionerkeys.go b/coderd/provisionerkeys.go index 3091e8f4f4b9f..b50d883ceb036 100644 --- a/coderd/provisionerkeys.go +++ b/coderd/provisionerkeys.go @@ -13,6 +13,14 @@ import ( "github.com/coder/coder/v2/codersdk" ) +// @Summary Create provisioner key +// @ID create-provisioner-key +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" +// @Success 201 {object} codersdk.CreateProvisionerKeyResponse +// @Router /organizations/{organization}/provisionerkey [post] func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) @@ -71,6 +79,14 @@ func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { }) } +// @Summary List provisioner key +// @ID list-provisioner-key +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" +// @Success 200 {object} []codersdk.ProvisionerKey +// @Router /organizations/{organization}/provisionerkey [get] func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) @@ -84,6 +100,15 @@ func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerKeys(pks)) } +// @Summary Delete provisioner key +// @ID delete-provisioner-key +// @Security CoderSessionToken +// @Produce json +// @Tags Enterprise +// @Param organization path string true "Organization ID" +// @Param provisionerkey path string true "Provisioner key name" +// @Success 204 +// @Router /organizations/{organization}/provisionerkey/{provisionerkey} [delete] func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) From 76cce3063dc1186afc07f5075b592e0503640750 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 18:27:57 +0000 Subject: [PATCH 10/20] move to enterprise --- coderd/coderd.go | 10 ------ codersdk/deployment.go | 2 ++ enterprise/coderd/coderd.go | 23 ++++++++++++- .../coderd}/provisionerkeys.go | 6 ++-- .../coderd}/provisionerkeys_test.go | 16 ++++++---- enterprise/coderd/templates.go | 32 ++++++++++--------- 6 files changed, 53 insertions(+), 36 deletions(-) rename {coderd => enterprise/coderd}/provisionerkeys.go (94%) rename {coderd => enterprise/coderd}/provisionerkeys_test.go (87%) diff --git a/coderd/coderd.go b/coderd/coderd.go index 56df04c14eb9e..0a3414fdb984c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -915,16 +915,6 @@ func New(options *Options) *API { }) }) }) - r.Route("/provisionerkeys", func(r chi.Router) { - r.Get("/", api.provisionerKeys) - r.Post("/", api.postProvisionerKey) - r.Route("/{provisionerkey}", func(r chi.Router) { - r.Use( - httpmw.ExtractProvisionerKeyParam(options.Database), - ) - r.Delete("/", api.deleteProvisionerKey) - }) - }) }) }) r.Route("/templates", func(r chi.Router) { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 8cf6681ad5954..23ba5bb7cf16a 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -56,6 +56,7 @@ const ( FeatureAccessControl FeatureName = "access_control" FeatureControlSharedPorts FeatureName = "control_shared_ports" FeatureCustomRoles FeatureName = "custom_roles" + FeatureMultipleOrganizations FeatureName = "multiple_organizations" ) // FeatureNames must be kept in-sync with the Feature enum above. @@ -77,6 +78,7 @@ var FeatureNames = []FeatureName{ FeatureAccessControl, FeatureControlSharedPorts, FeatureCustomRoles, + FeatureMultipleOrganizations, } // Humanize returns the feature name in a human-readable format. diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 89cc82b73f68e..9d9d0ffa45dbe 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -205,7 +205,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { }) r.Route("/workspaceproxies", func(r chi.Router) { r.Use( - api.moonsEnabledMW, + api.RequireFeatureMW(codersdk.FeatureWorkspaceProxy), ) r.Group(func(r chi.Router) { r.Use( @@ -254,6 +254,21 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Get("/", api.groupByOrganization) }) }) + r.Route("/organizations/{organization}/provisionerkeys", func(r chi.Router) { + r.Use( + apiKeyMiddleware, + httpmw.ExtractOrganizationParam(api.Database), + api.RequireFeatureMW(codersdk.FeatureMultipleOrganizations), + ) + r.Get("/", api.provisionerKeys) + r.Post("/", api.postProvisionerKey) + r.Route("/{provisionerkey}", func(r chi.Router) { + r.Use( + httpmw.ExtractProvisionerKeyParam(options.Database), + ) + r.Delete("/", api.deleteProvisionerKey) + }) + }) // TODO: provisioner daemons are not scoped to organizations in the database, so placing them // under an organization route doesn't make sense. In order to allow the /serve endpoint to // work with a pre-shared key (PSK) without an API key, these routes will simply ignore the @@ -566,6 +581,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { codersdk.FeatureUserRoleManagement: true, codersdk.FeatureAccessControl: true, codersdk.FeatureControlSharedPorts: true, + codersdk.FeatureMultipleOrganizations: true, }) if err != nil { return err @@ -751,6 +767,11 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.AGPL.CustomRoleHandler.Store(&handler) } + if initial, changed, enabled := featureChanged(codersdk.FeatureMultipleOrganizations); shouldUpdate(initial, changed, enabled) { + var handler coderd.CustomRoleHandler = &enterpriseCustomRoleHandler{API: api, Enabled: enabled} + api.AGPL.CustomRoleHandler.Store(&handler) + } + // External token encryption is soft-enforced featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption] featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0 diff --git a/coderd/provisionerkeys.go b/enterprise/coderd/provisionerkeys.go similarity index 94% rename from coderd/provisionerkeys.go rename to enterprise/coderd/provisionerkeys.go index b50d883ceb036..2730ed2638e6c 100644 --- a/coderd/provisionerkeys.go +++ b/enterprise/coderd/provisionerkeys.go @@ -20,7 +20,7 @@ import ( // @Tags Enterprise // @Param organization path string true "Organization ID" // @Success 201 {object} codersdk.CreateProvisionerKeyResponse -// @Router /organizations/{organization}/provisionerkey [post] +// @Router /organizations/{organization}/provisionerkeys [post] func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) @@ -86,7 +86,7 @@ func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { // @Tags Enterprise // @Param organization path string true "Organization ID" // @Success 200 {object} []codersdk.ProvisionerKey -// @Router /organizations/{organization}/provisionerkey [get] +// @Router /organizations/{organization}/provisionerkeys [get] func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) @@ -108,7 +108,7 @@ func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) { // @Param organization path string true "Organization ID" // @Param provisionerkey path string true "Provisioner key name" // @Success 204 -// @Router /organizations/{organization}/provisionerkey/{provisionerkey} [delete] +// @Router /organizations/{organization}/provisionerkeys/{provisionerkey} [delete] func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) diff --git a/coderd/provisionerkeys_test.go b/enterprise/coderd/provisionerkeys_test.go similarity index 87% rename from coderd/provisionerkeys_test.go rename to enterprise/coderd/provisionerkeys_test.go index f406114ec13ed..d7ab8223ed8b0 100644 --- a/coderd/provisionerkeys_test.go +++ b/enterprise/coderd/provisionerkeys_test.go @@ -9,6 +9,8 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/coder/v2/testutil" ) @@ -17,18 +19,18 @@ func TestProvisionerKeys(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*10) t.Cleanup(cancel) - client := coderdtest.New(t, nil) - owner := coderdtest.CreateFirstUser(t, client) + client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }}) orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ - Name: "other", - }) - require.NoError(t, err, "create org") + otherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) outsideOrgAdmin, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.ScopedRoleOrgAdmin(otherOrg.ID)) // member cannot create a provisioner key - _, err = member.CreateProvisionerKey(ctx, otherOrg.ID, codersdk.CreateProvisionerKeyRequest{ + _, err := member.CreateProvisionerKey(ctx, otherOrg.ID, codersdk.CreateProvisionerKeyRequest{ Name: "key", }) require.ErrorContains(t, err, "Resource not found") diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go index d9d5f245fcb41..9531125d7ceb1 100644 --- a/enterprise/coderd/templates.go +++ b/enterprise/coderd/templates.go @@ -327,7 +327,7 @@ func convertSDKTemplateRole(role codersdk.TemplateRole) []policy.Action { return nil } -// TODO reduce the duplication across all of these. +// TODO move to api.RequireFeatureMW when we are OK with changing the behavior. func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { api.entitlementsMu.RLock() @@ -343,19 +343,21 @@ func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler { }) } -func (api *API) moonsEnabledMW(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - // Entitlement must be enabled. - api.entitlementsMu.RLock() - proxy := api.entitlements.Features[codersdk.FeatureWorkspaceProxy].Enabled - api.entitlementsMu.RUnlock() - if !proxy { - httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ - Message: "External workspace proxies is an Enterprise feature. Contact sales!", - }) - return - } +func (api *API) RequireFeatureMW(feat codersdk.FeatureName) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // Entitlement must be enabled. + api.entitlementsMu.RLock() + enabled := api.entitlements.Features[feat].Enabled + api.entitlementsMu.RUnlock() + if !enabled { + httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ + Message: fmt.Sprintf("%s is an Enterprise feature. Contact sales!", feat.Humanize()), + }) + return + } - next.ServeHTTP(rw, r) - }) + next.ServeHTTP(rw, r) + }) + } } From e2b2d6b1257406e3915a59d03d3923a641ab1a38 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 20:08:12 +0000 Subject: [PATCH 11/20] dbauthz --- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbauthz/dbauthz_test.go | 52 +++++++++++++++++++++++++ coderd/database/dbgen/dbgen.go | 12 ++++++ coderd/database/dbmem/dbmem.go | 2 +- coderd/rbac/roles_test.go | 9 +++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 7f48b7e5e5687..211bb5dfce71c 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2628,7 +2628,7 @@ func (q *querier) InsertProvisionerJobLogs(ctx context.Context, arg database.Ins } func (q *querier) InsertProvisionerKey(ctx context.Context, arg database.InsertProvisionerKeyParams) (database.ProvisionerKey, error) { - return insert(q.log, q.auth, rbac.ResourceProvisionerKeys.InOrg(arg.OrganizationID), q.db.InsertProvisionerKey)(ctx, arg) + return insert(q.log, q.auth, rbac.ResourceProvisionerKeys.InOrg(arg.OrganizationID).WithID(arg.ID), q.db.InsertProvisionerKey)(ctx, arg) } func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 52d375116e6a3..f97c31e517029 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1800,6 +1800,58 @@ func (s *MethodTestSuite) TestWorkspacePortSharing() { })) } +func (s *MethodTestSuite) TestProvisionerKeys() { + s.Run("InsertProvisionerKey", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + pk := database.ProvisionerKey{ + ID: uuid.New(), + CreatedAt: time.Now(), + OrganizationID: org.ID, + Name: coderdtest.RandomName(s.T()), + HashedSecret: []byte(coderdtest.RandomName(s.T())), + } + //nolint:gosimple // casting is not a simplification + check.Args(database.InsertProvisionerKeyParams{ + ID: pk.ID, + CreatedAt: pk.CreatedAt, + OrganizationID: pk.OrganizationID, + Name: pk.Name, + HashedSecret: pk.HashedSecret, + }).Asserts(pk, policy.ActionCreate).Returns(pk) + })) + s.Run("GetProvisionerKeyByID", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) + check.Args(pk.ID).Asserts(pk, policy.ActionRead).Returns(pk) + })) + s.Run("GetProvisionerKeyByName", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) + check.Args(database.GetProvisionerKeyByNameParams{ + OrganizationID: org.ID, + Name: pk.Name, + }).Asserts(pk, policy.ActionRead).Returns(pk) + })) + s.Run("ListProvisionerKeysByOrganization", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) + pks := []database.ListProvisionerKeysByOrganizationRow{ + { + ID: pk.ID, + CreatedAt: pk.CreatedAt, + OrganizationID: pk.OrganizationID, + Name: pk.Name, + }, + } + check.Args(org.ID).Asserts(pk, policy.ActionRead).Returns(pks) + })) + s.Run("DeleteProvisionerKey", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) + check.Args(pk.ID).Asserts(pk, policy.ActionDelete).Returns() + })) +} + func (s *MethodTestSuite) TestExtraMethods() { s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) { d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{ diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index d2b66e5d4b6df..29f7b1f2e5a69 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -465,6 +465,18 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data return job } +func ProvisionerKey(t testing.TB, db database.Store, orig database.ProvisionerKey) database.ProvisionerKey { + key, err := db.InsertProvisionerKey(genCtx, database.InsertProvisionerKeyParams{ + ID: takeFirst(orig.ID, uuid.New()), + CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), + OrganizationID: takeFirst(orig.OrganizationID, uuid.New()), + Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)), + HashedSecret: orig.HashedSecret, + }) + require.NoError(t, err, "insert provisioner key") + return key +} + func WorkspaceApp(t testing.TB, db database.Store, orig database.WorkspaceApp) database.WorkspaceApp { resource, err := db.InsertWorkspaceApp(genCtx, database.InsertWorkspaceAppParams{ ID: takeFirst(orig.ID, uuid.New()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 25d791800ddd4..805a0b200fc7f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6556,7 +6556,7 @@ func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.Inser CreatedAt: arg.CreatedAt, OrganizationID: arg.OrganizationID, Name: arg.Name, - HashedSecret: []byte(arg.Name), + HashedSecret: arg.HashedSecret, } q.provisionerKeys = append(q.provisionerKeys, provisionerKey) diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index c49f161760235..cedb3d8e1af79 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -488,6 +488,15 @@ func TestRolePermissions(t *testing.T) { false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, }, }, + { + Name: "ProvisionerKeys", + Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, + Resource: rbac.ResourceProvisionerKeys.InOrg(orgID), + AuthorizeMap: map[bool][]authSubject{ + true: {owner, orgAdmin}, + false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin, templateAdmin}, + }, + }, { Name: "System", Actions: crud, From 5f5e9233781f67b3729b03d42729a320b36282b4 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 20:15:33 +0000 Subject: [PATCH 12/20] fix swagger --- coderd/apidoc/docs.go | 132 +++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 122 +++++++++++++++++++++++++ docs/api/enterprise.md | 118 ++++++++++++++++++++++++ docs/api/schemas.md | 34 +++++++ enterprise/coderd/provisionerkeys.go | 1 - site/src/api/typesGenerated.ts | 2 + 6 files changed, 408 insertions(+), 1 deletion(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 353f1584efb33..7599b42f14f6b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2676,6 +2676,110 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/provisionerkeys": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "List provisioner key", + "operationId": "list-provisioner-key", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerKey" + } + } + } + } + }, + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "Create provisioner key", + "operationId": "create-provisioner-key", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/codersdk.CreateProvisionerKeyResponse" + } + } + } + } + }, + "/organizations/{organization}/provisionerkeys/{provisionerkey}": { + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Enterprise" + ], + "summary": "Delete provisioner key", + "operationId": "delete-provisioner-key", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Provisioner key name", + "name": "provisionerkey", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -8609,6 +8713,14 @@ const docTemplate = `{ } } }, + "codersdk.CreateProvisionerKeyResponse": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + } + }, "codersdk.CreateTemplateRequest": { "type": "object", "required": [ @@ -10762,6 +10874,26 @@ const docTemplate = `{ "ProvisionerJobUnknown" ] }, + "codersdk.ProvisionerKey": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.ProvisionerLogLevel": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 597facb6e3cad..85e57932a445d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2346,6 +2346,100 @@ } } }, + "/organizations/{organization}/provisionerkeys": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "List provisioner key", + "operationId": "list-provisioner-key", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.ProvisionerKey" + } + } + } + } + }, + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "Create provisioner key", + "operationId": "create-provisioner-key", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/codersdk.CreateProvisionerKeyResponse" + } + } + } + } + }, + "/organizations/{organization}/provisionerkeys/{provisionerkey}": { + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Enterprise"], + "summary": "Delete provisioner key", + "operationId": "delete-provisioner-key", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Provisioner key name", + "name": "provisionerkey", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/organizations/{organization}/templates": { "get": { "security": [ @@ -7661,6 +7755,14 @@ } } }, + "codersdk.CreateProvisionerKeyResponse": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + } + }, "codersdk.CreateTemplateRequest": { "type": "object", "required": ["name", "template_version_id"], @@ -9702,6 +9804,26 @@ "ProvisionerJobUnknown" ] }, + "codersdk.ProvisionerKey": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string", + "format": "uuid" + } + } + }, "codersdk.ProvisionerLogLevel": { "type": "string", "enum": ["debug"], diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index 63786efac43db..3f6013d46cfb5 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -1353,6 +1353,124 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi To perform this operation, you must be authenticated. [Learn more](authentication.md). +## List provisioner key + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisionerkeys \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/provisionerkeys` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------ | -------- | --------------- | +| `organization` | path | string | true | Organization ID | + +### Example responses + +> 200 Response + +```json +[ + { + "created_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "organization": "452c1a86-a0af-475b-b03f-724878b0f387" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.ProvisionerKey](schemas.md#codersdkprovisionerkey) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| ---------------- | ----------------- | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | +| `» created_at` | string(date-time) | false | | | +| `» id` | string(uuid) | false | | | +| `» name` | string | false | | | +| `» organization` | string(uuid) | false | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Create provisioner key + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/provisionerkeys \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /organizations/{organization}/provisionerkeys` + +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------ | -------- | --------------- | +| `organization` | path | string | true | Organization ID | + +### Example responses + +> 201 Response + +```json +{ + "key": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------- | +| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.CreateProvisionerKeyResponse](schemas.md#codersdkcreateprovisionerkeyresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Delete provisioner key + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization}/provisionerkeys/{provisionerkey} \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /organizations/{organization}/provisionerkeys/{provisionerkey}` + +### Parameters + +| Name | In | Type | Required | Description | +| ---------------- | ---- | ------ | -------- | -------------------- | +| `organization` | path | string | true | Organization ID | +| `provisionerkey` | path | string | true | Provisioner key name | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | --------------------------------------------------------------- | ----------- | ------ | +| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get active replicas ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 19687dd55db8a..125ca94eb98dd 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1047,6 +1047,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `icon` | string | false | | | | `name` | string | true | | | +## codersdk.CreateProvisionerKeyResponse + +```json +{ + "key": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ----- | ------ | -------- | ------------ | ----------- | +| `key` | string | false | | | + ## codersdk.CreateTemplateRequest ```json @@ -3810,6 +3824,26 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `failed` | | `unknown` | +## codersdk.ProvisionerKey + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "name": "string", + "organization": "452c1a86-a0af-475b-b03f-724878b0f387" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------ | -------- | ------------ | ----------- | +| `created_at` | string | false | | | +| `id` | string | false | | | +| `name` | string | false | | | +| `organization` | string | false | | | + ## codersdk.ProvisionerLogLevel ```json diff --git a/enterprise/coderd/provisionerkeys.go b/enterprise/coderd/provisionerkeys.go index 2730ed2638e6c..553eee7e3efae 100644 --- a/enterprise/coderd/provisionerkeys.go +++ b/enterprise/coderd/provisionerkeys.go @@ -103,7 +103,6 @@ func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) { // @Summary Delete provisioner key // @ID delete-provisioner-key // @Security CoderSessionToken -// @Produce json // @Tags Enterprise // @Param organization path string true "Organization ID" // @Param provisionerkey path string true "Provisioner key name" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index aa6b471b5b54d..3963c2b4f5a01 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2047,6 +2047,7 @@ export type FeatureName = | "external_token_encryption" | "high_availability" | "multiple_external_auth" + | "multiple_organizations" | "scim" | "template_rbac" | "user_limit" @@ -2065,6 +2066,7 @@ export const FeatureNames: FeatureName[] = [ "external_token_encryption", "high_availability", "multiple_external_auth", + "multiple_organizations", "scim", "template_rbac", "user_limit", From 81d4e92f9cd6c44a02e7a31712eb027c7293f52b Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 20:26:09 +0000 Subject: [PATCH 13/20] put routes under experiment --- coderd/database/dbmem/dbmem.go | 1 + enterprise/coderd/coderd.go | 1 + enterprise/coderd/provisionerkeys_test.go | 15 +++++++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 805a0b200fc7f..b6dbf473dfc0b 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6551,6 +6551,7 @@ func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.Inser } } + //nolint:gosimple provisionerKey := database.ProvisionerKey{ ID: arg.ID, CreatedAt: arg.CreatedAt, diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 9d9d0ffa45dbe..784695a7ac2e3 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -259,6 +259,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { apiKeyMiddleware, httpmw.ExtractOrganizationParam(api.Database), api.RequireFeatureMW(codersdk.FeatureMultipleOrganizations), + httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentMultiOrganization), ) r.Get("/", api.provisionerKeys) r.Post("/", api.postProvisionerKey) diff --git a/enterprise/coderd/provisionerkeys_test.go b/enterprise/coderd/provisionerkeys_test.go index d7ab8223ed8b0..ff62b38695409 100644 --- a/enterprise/coderd/provisionerkeys_test.go +++ b/enterprise/coderd/provisionerkeys_test.go @@ -19,11 +19,18 @@ func TestProvisionerKeys(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*10) t.Cleanup(cancel) - client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureMultipleOrganizations: 1, + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, }, - }}) + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) otherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{}) From efcf83221a713e010a02c629939879e961d1234f Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 20:31:36 +0000 Subject: [PATCH 14/20] add fixture --- .../testdata/fixtures/000226_provisioner_keys.up.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql diff --git a/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql b/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql new file mode 100644 index 0000000000000..c649412c8d686 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql @@ -0,0 +1,4 @@ +INSERT INTO provisioner_keys + (id, created_at, organization_id, name, hashed_secret) +VALUES + ('b90547be-8870-4d68-8184-e8b2242b7c01', '2021-06-01 00:00:00', 'b90547be-8870-4d68-8184-e8b2242b7c01', 'qua', '\xDEADBEEF'::bytea); From 3530f73a447feda3924949916cf732cf03f09305 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 12 Jul 2024 20:39:24 +0000 Subject: [PATCH 15/20] use existing org id --- .../migrations/testdata/fixtures/000226_provisioner_keys.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql b/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql index c649412c8d686..418e519677518 100644 --- a/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql +++ b/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql @@ -1,4 +1,4 @@ INSERT INTO provisioner_keys (id, created_at, organization_id, name, hashed_secret) VALUES - ('b90547be-8870-4d68-8184-e8b2242b7c01', '2021-06-01 00:00:00', 'b90547be-8870-4d68-8184-e8b2242b7c01', 'qua', '\xDEADBEEF'::bytea); + ('b90547be-8870-4d68-8184-e8b2242b7c01', '2021-06-01 00:00:00', 'bb640d07-ca8a-4869-b6bc-ae61ebb2fda1', 'qua', '\xDEADBEEF'::bytea); From e5f73112c5d4be584cf720c587d3ba187a27112c Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 15 Jul 2024 21:13:39 +0000 Subject: [PATCH 16/20] save --- coderd/database/migrations/000226_provisioner_keys.up.sql | 3 ++- coderd/database/modelmethods.go | 6 ------ coderd/database/queries/provisionerkeys.sql | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/coderd/database/migrations/000226_provisioner_keys.up.sql b/coderd/database/migrations/000226_provisioner_keys.up.sql index 39e327a5341d7..adef8ffa16795 100644 --- a/coderd/database/migrations/000226_provisioner_keys.up.sql +++ b/coderd/database/migrations/000226_provisioner_keys.up.sql @@ -4,5 +4,6 @@ CREATE TABLE provisioner_keys ( organization_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, name varchar(64) NOT NULL, hashed_secret bytea NOT NULL, - UNIQUE (organization_id, name) ); + +CREATE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys (organization_id, name); diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index dbf06518500ff..85d08cbfba8ec 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -218,12 +218,6 @@ func (p ProvisionerKey) RBACObject() rbac.Object { InOrg(p.OrganizationID) } -func (p ListProvisionerKeysByOrganizationRow) RBACObject() rbac.Object { - return rbac.ResourceProvisionerKeys. - WithID(p.ID). - InOrg(p.OrganizationID) -} - func (w WorkspaceProxy) RBACObject() rbac.Object { return rbac.ResourceWorkspaceProxy. WithID(w.ID) diff --git a/coderd/database/queries/provisionerkeys.sql b/coderd/database/queries/provisionerkeys.sql index 5a6eea50ca159..3894e5b98c5cb 100644 --- a/coderd/database/queries/provisionerkeys.sql +++ b/coderd/database/queries/provisionerkeys.sql @@ -8,7 +8,7 @@ INSERT INTO hashed_secret ) VALUES - ($1, $2, $3, $4, $5) RETURNING *; + ($1, $2, $3, lower($4), $5) RETURNING *; -- name: GetProvisionerKeyByID :one SELECT From 28ded4534b96b135e4fb1cc009aa08da59153e83 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 15 Jul 2024 22:06:23 +0000 Subject: [PATCH 17/20] fix case sensitivity --- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbauthz/dbauthz_test.go | 2 +- coderd/database/dbmem/dbmem.go | 21 ++++++++++----- coderd/database/dbmetrics/dbmetrics.go | 2 +- coderd/database/dbmock/dbmock.go | 4 +-- coderd/database/dump.sql | 5 ++-- .../migrations/000226_provisioner_keys.up.sql | 4 +-- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 27 +++++++------------ coderd/database/queries/provisionerkeys.sql | 9 +++---- coderd/database/unique_constraint.go | 2 +- codersdk/provisionerdaemons.go | 1 + enterprise/coderd/provisionerkeys.go | 5 ++-- enterprise/coderd/provisionerkeys_test.go | 4 +-- 14 files changed, 44 insertions(+), 46 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 211bb5dfce71c..9f2de9088b5c5 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2859,7 +2859,7 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab return q.db.InsertWorkspaceResourceMetadata(ctx, arg) } -func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { +func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID) } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index f97c31e517029..22afeb2e67552 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1835,7 +1835,7 @@ func (s *MethodTestSuite) TestProvisionerKeys() { s.Run("ListProvisionerKeysByOrganization", s.Subtest(func(db database.Store, check *expects) { org := dbgen.Organization(s.T(), db, database.Organization{}) pk := dbgen.ProvisionerKey(s.T(), db, database.ProvisionerKey{OrganizationID: org.ID}) - pks := []database.ListProvisionerKeysByOrganizationRow{ + pks := []database.ProvisionerKey{ { ID: pk.ID, CreatedAt: pk.CreatedAt, diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b6dbf473dfc0b..08e86b546474f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -124,6 +124,14 @@ func (inTxMutex) RLock() {} func (inTxMutex) Unlock() {} func (inTxMutex) RUnlock() {} +// newUniqueContraintError copies the base unique constraint error and sets the constraint to the provided value. +func newUniqueContraintError(uc database.UniqueConstraint) *pq.Error { + newErr := *errUniqueConstraint + newErr.Constraint = string(uc) + + return &newErr +} + // FakeQuerier replicates database functionality to enable quick testing. It's an exported type so that our test code // can do type checks. type FakeQuerier struct { @@ -3228,7 +3236,7 @@ func (q *FakeQuerier) GetProvisionerKeyByName(_ context.Context, arg database.Ge defer q.mutex.RUnlock() for _, key := range q.provisionerKeys { - if key.Name == arg.Name && key.OrganizationID == arg.OrganizationID { + if strings.ToLower(key.Name) == strings.ToLower(arg.Name) && key.OrganizationID == arg.OrganizationID { return key, nil } } @@ -6544,7 +6552,7 @@ func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.Inser defer q.mutex.Unlock() newErr := *errUniqueConstraint - newErr.Constraint = string(database.UniqueProvisionerKeysOrganizationIDNameKey) + newErr.Constraint = string(database.UniqueProvisionerKeysOrganizationIDNameIndex) for _, key := range q.provisionerKeys { if key.ID == arg.ID || (key.OrganizationID == arg.OrganizationID && key.Name == arg.Name) { return database.ProvisionerKey{}, &newErr @@ -6556,7 +6564,7 @@ func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.Inser ID: arg.ID, CreatedAt: arg.CreatedAt, OrganizationID: arg.OrganizationID, - Name: arg.Name, + Name: strings.ToLower(arg.Name), HashedSecret: arg.HashedSecret, } q.provisionerKeys = append(q.provisionerKeys, provisionerKey) @@ -7241,18 +7249,19 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat return metadata, nil } -func (q *FakeQuerier) ListProvisionerKeysByOrganization(_ context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { +func (q *FakeQuerier) ListProvisionerKeysByOrganization(_ context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { q.mutex.RLock() defer q.mutex.RUnlock() - keys := make([]database.ListProvisionerKeysByOrganizationRow, 0) + keys := make([]database.ProvisionerKey, 0) for _, key := range q.provisionerKeys { if key.OrganizationID == organizationID { - keys = append(keys, database.ListProvisionerKeysByOrganizationRow{ + keys = append(keys, database.ProvisionerKey{ ID: key.ID, CreatedAt: key.CreatedAt, OrganizationID: key.OrganizationID, Name: key.Name, + HashedSecret: key.HashedSecret, }) } } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index a46cc3ee5cdf7..f249a78436153 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1831,7 +1831,7 @@ func (m metricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, arg d return metadata, err } -func (m metricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { +func (m metricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { start := time.Now() r0, r1 := m.s.ListProvisionerKeysByOrganization(ctx, organizationID) m.queryLatencies.WithLabelValues("ListProvisionerKeysByOrganization").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 83a348c66abd5..093869e655583 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3838,10 +3838,10 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(arg0, arg1 any) } // ListProvisionerKeysByOrganization mocks base method. -func (m *MockStore) ListProvisionerKeysByOrganization(arg0 context.Context, arg1 uuid.UUID) ([]database.ListProvisionerKeysByOrganizationRow, error) { +func (m *MockStore) ListProvisionerKeysByOrganization(arg0 context.Context, arg1 uuid.UUID) ([]database.ProvisionerKey, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListProvisionerKeysByOrganization", arg0, arg1) - ret0, _ := ret[0].([]database.ListProvisionerKeysByOrganizationRow) + ret0, _ := ret[0].([]database.ProvisionerKey) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 52d4519c99dfc..c7f3f52e91033 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1592,9 +1592,6 @@ ALTER TABLE ONLY provisioner_job_logs ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); -ALTER TABLE ONLY provisioner_keys - ADD CONSTRAINT provisioner_keys_organization_id_name_key UNIQUE (organization_id, name); - ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); @@ -1757,6 +1754,8 @@ CREATE INDEX provisioner_job_logs_id_job_id_idx ON provisioner_job_logs USING bt CREATE INDEX provisioner_jobs_started_at_idx ON provisioner_jobs USING btree (started_at) WHERE (started_at IS NULL); +CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, name); + CREATE INDEX template_usage_stats_start_time_idx ON template_usage_stats USING btree (start_time DESC); COMMENT ON INDEX template_usage_stats_start_time_idx IS 'Index for querying MAX(start_time).'; diff --git a/coderd/database/migrations/000226_provisioner_keys.up.sql b/coderd/database/migrations/000226_provisioner_keys.up.sql index adef8ffa16795..62a735b4b3ac5 100644 --- a/coderd/database/migrations/000226_provisioner_keys.up.sql +++ b/coderd/database/migrations/000226_provisioner_keys.up.sql @@ -3,7 +3,7 @@ CREATE TABLE provisioner_keys ( created_at timestamptz NOT NULL, organization_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, name varchar(64) NOT NULL, - hashed_secret bytea NOT NULL, + hashed_secret bytea NOT NULL ); -CREATE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys (organization_id, name); +CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys (organization_id, name); diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c7e003f16b361..2a8153deec2d1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -374,7 +374,7 @@ type sqlcQuerier interface { InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) - ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ListProvisionerKeysByOrganizationRow, error) + ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) // Arguments are optional with uuid.Nil to ignore. // - Use just 'organization_id' to get all members of an org diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 28c257d2e0bd7..55930cdc39ebd 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5497,7 +5497,7 @@ FROM WHERE organization_id = $1 AND - name = $2 + name = lower($2) ` type GetProvisionerKeyByNameParams struct { @@ -5528,15 +5528,15 @@ INSERT INTO hashed_secret ) VALUES - ($1, $2, $3, $4, $5) RETURNING id, created_at, organization_id, name, hashed_secret + ($1, $2, $3, lower($5), $4) RETURNING id, created_at, organization_id, name, hashed_secret ` type InsertProvisionerKeyParams struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Name string `db:"name" json:"name"` HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"` + Name string `db:"name" json:"name"` } func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) { @@ -5544,8 +5544,8 @@ func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisi arg.ID, arg.CreatedAt, arg.OrganizationID, - arg.Name, arg.HashedSecret, + arg.Name, ) var i ProvisionerKey err := row.Scan( @@ -5560,37 +5560,28 @@ func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisi const listProvisionerKeysByOrganization = `-- name: ListProvisionerKeysByOrganization :many SELECT - id, - created_at, - organization_id, - name + id, created_at, organization_id, name, hashed_secret FROM provisioner_keys WHERE organization_id = $1 ` -type ListProvisionerKeysByOrganizationRow struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Name string `db:"name" json:"name"` -} - -func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ListProvisionerKeysByOrganizationRow, error) { +func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) { rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganization, organizationID) if err != nil { return nil, err } defer rows.Close() - var items []ListProvisionerKeysByOrganizationRow + var items []ProvisionerKey for rows.Next() { - var i ListProvisionerKeysByOrganizationRow + var i ProvisionerKey if err := rows.Scan( &i.ID, &i.CreatedAt, &i.OrganizationID, &i.Name, + &i.HashedSecret, ); err != nil { return nil, err } diff --git a/coderd/database/queries/provisionerkeys.sql b/coderd/database/queries/provisionerkeys.sql index 3894e5b98c5cb..6fa3ef1b1cd48 100644 --- a/coderd/database/queries/provisionerkeys.sql +++ b/coderd/database/queries/provisionerkeys.sql @@ -8,7 +8,7 @@ INSERT INTO hashed_secret ) VALUES - ($1, $2, $3, lower($4), $5) RETURNING *; + ($1, $2, $3, lower(@name), $4) RETURNING *; -- name: GetProvisionerKeyByID :one SELECT @@ -26,14 +26,11 @@ FROM WHERE organization_id = $1 AND - name = $2; + name = lower(@name); -- name: ListProvisionerKeysByOrganization :many SELECT - id, - created_at, - organization_id, - name + * FROM provisioner_keys WHERE diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 6702e862e5378..4fc6d15243fc8 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -44,7 +44,6 @@ const ( UniqueProvisionerDaemonsPkey UniqueConstraint = "provisioner_daemons_pkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_pkey PRIMARY KEY (id); UniqueProvisionerJobLogsPkey UniqueConstraint = "provisioner_job_logs_pkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_pkey PRIMARY KEY (id); UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); - UniqueProvisionerKeysOrganizationIDNameKey UniqueConstraint = "provisioner_keys_organization_id_name_key" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_name_key UNIQUE (organization_id, name); UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id); UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); UniqueTailnetAgentsPkey UniqueConstraint = "tailnet_agents_pkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_pkey PRIMARY KEY (id, coordinator_id); @@ -89,6 +88,7 @@ const ( UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); + UniqueProvisionerKeysOrganizationIDNameIndex UniqueConstraint = "provisioner_keys_organization_id_name_idx" // CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, name); UniqueTemplateUsageStatsStartTimeTemplateIDUserIDIndex UniqueConstraint = "template_usage_stats_start_time_template_id_user_id_idx" // CREATE UNIQUE INDEX template_usage_stats_start_time_template_id_user_id_idx ON template_usage_stats USING btree (start_time, template_id, user_id); UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false); UniqueUserLinksLinkedIDLoginTypeIndex UniqueConstraint = "user_links_linked_id_login_type_idx" // CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 0bd66cf3742d3..605d44d88c071 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -271,6 +271,7 @@ type ProvisionerKey struct { CreatedAt time.Time `json:"created_at" format:"date-time"` OrganizationID uuid.UUID `json:"organization" format:"uuid"` Name string `json:"name"` + // HashedSecret - never include the access token in the API response } type CreateProvisionerKeyRequest struct { diff --git a/enterprise/coderd/provisionerkeys.go b/enterprise/coderd/provisionerkeys.go index 553eee7e3efae..d107aecb1e226 100644 --- a/enterprise/coderd/provisionerkeys.go +++ b/enterprise/coderd/provisionerkeys.go @@ -63,7 +63,7 @@ func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) { } _, err = api.Database.InsertProvisionerKey(ctx, params) - if database.IsUniqueViolation(err, database.UniqueProvisionerKeysOrganizationIDNameKey) { + if database.IsUniqueViolation(err, database.UniqueProvisionerKeysOrganizationIDNameIndex) { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: fmt.Sprintf("Provisioner key with name '%s' already exists in organization", req.Name), }) @@ -135,7 +135,7 @@ func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusNoContent, nil) } -func convertProvisionerKeys(dbKeys []database.ListProvisionerKeysByOrganizationRow) []codersdk.ProvisionerKey { +func convertProvisionerKeys(dbKeys []database.ProvisionerKey) []codersdk.ProvisionerKey { keys := make([]codersdk.ProvisionerKey, 0, len(dbKeys)) for _, dbKey := range dbKeys { keys = append(keys, codersdk.ProvisionerKey{ @@ -143,6 +143,7 @@ func convertProvisionerKeys(dbKeys []database.ListProvisionerKeysByOrganizationR CreatedAt: dbKey.CreatedAt, OrganizationID: dbKey.OrganizationID, Name: dbKey.Name, + // HashedSecret - never include the access token in the API response }) } return keys diff --git a/enterprise/coderd/provisionerkeys_test.go b/enterprise/coderd/provisionerkeys_test.go index ff62b38695409..f40d4ff2951df 100644 --- a/enterprise/coderd/provisionerkeys_test.go +++ b/enterprise/coderd/provisionerkeys_test.go @@ -71,7 +71,7 @@ func TestProvisionerKeys(t *testing.T) { // org admin can create a provisioner key _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ - Name: "key", + Name: "Key", // case insensitive }) require.NoError(t, err, "org admin create provisioner key") @@ -99,7 +99,7 @@ func TestProvisionerKeys(t *testing.T) { require.Len(t, keys, 1, "org admin list provisioner keys") // org admin can delete a provisioner key - err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") + err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") // using lowercase here works require.NoError(t, err, "org admin delete provisioner key") // org admin cannot delete a provisioner key that doesn't exist From 1d246ec0cf35006f0e6326cbe5f3883172b1841d Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 16 Jul 2024 16:05:22 +0000 Subject: [PATCH 18/20] fix tests and pr comments --- coderd/database/dbmem/dbmem.go | 23 ++++++++----------- .../migrations/000226_provisioner_keys.up.sql | 2 +- coderd/database/queries/provisionerkeys.sql | 2 +- enterprise/coderd/provisionerkeys.go | 5 ++-- enterprise/coderd/provisionerkeys_test.go | 2 +- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 08e86b546474f..198b4b4f3b6a9 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -124,14 +124,6 @@ func (inTxMutex) RLock() {} func (inTxMutex) Unlock() {} func (inTxMutex) RUnlock() {} -// newUniqueContraintError copies the base unique constraint error and sets the constraint to the provided value. -func newUniqueContraintError(uc database.UniqueConstraint) *pq.Error { - newErr := *errUniqueConstraint - newErr.Constraint = string(uc) - - return &newErr -} - // FakeQuerier replicates database functionality to enable quick testing. It's an exported type so that our test code // can do type checks. type FakeQuerier struct { @@ -277,6 +269,13 @@ func validateDatabaseType(args interface{}) error { return nil } +func newUniqueConstraintError(uc database.UniqueConstraint) *pq.Error { + newErr := *errUniqueConstraint + newErr.Constraint = string(uc) + + return &newErr +} + func (*FakeQuerier) Ping(_ context.Context) (time.Duration, error) { return 0, nil } @@ -3236,7 +3235,7 @@ func (q *FakeQuerier) GetProvisionerKeyByName(_ context.Context, arg database.Ge defer q.mutex.RUnlock() for _, key := range q.provisionerKeys { - if strings.ToLower(key.Name) == strings.ToLower(arg.Name) && key.OrganizationID == arg.OrganizationID { + if strings.EqualFold(key.Name, arg.Name) && key.OrganizationID == arg.OrganizationID { return key, nil } } @@ -6551,11 +6550,9 @@ func (q *FakeQuerier) InsertProvisionerKey(_ context.Context, arg database.Inser q.mutex.Lock() defer q.mutex.Unlock() - newErr := *errUniqueConstraint - newErr.Constraint = string(database.UniqueProvisionerKeysOrganizationIDNameIndex) for _, key := range q.provisionerKeys { - if key.ID == arg.ID || (key.OrganizationID == arg.OrganizationID && key.Name == arg.Name) { - return database.ProvisionerKey{}, &newErr + if key.ID == arg.ID || (key.OrganizationID == arg.OrganizationID && strings.EqualFold(key.Name, arg.Name)) { + return database.ProvisionerKey{}, newUniqueConstraintError(database.UniqueProvisionerKeysOrganizationIDNameIndex) } } diff --git a/coderd/database/migrations/000226_provisioner_keys.up.sql b/coderd/database/migrations/000226_provisioner_keys.up.sql index 62a735b4b3ac5..44942f729f19b 100644 --- a/coderd/database/migrations/000226_provisioner_keys.up.sql +++ b/coderd/database/migrations/000226_provisioner_keys.up.sql @@ -6,4 +6,4 @@ CREATE TABLE provisioner_keys ( hashed_secret bytea NOT NULL ); -CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys (organization_id, name); +CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower(name)); diff --git a/coderd/database/queries/provisionerkeys.sql b/coderd/database/queries/provisionerkeys.sql index 6fa3ef1b1cd48..22e714eca350d 100644 --- a/coderd/database/queries/provisionerkeys.sql +++ b/coderd/database/queries/provisionerkeys.sql @@ -26,7 +26,7 @@ FROM WHERE organization_id = $1 AND - name = lower(@name); + lower(name) = lower(@name); -- name: ListProvisionerKeysByOrganization :many SELECT diff --git a/enterprise/coderd/provisionerkeys.go b/enterprise/coderd/provisionerkeys.go index d107aecb1e226..9cb66e2b7910d 100644 --- a/enterprise/coderd/provisionerkeys.go +++ b/enterprise/coderd/provisionerkeys.go @@ -1,8 +1,6 @@ package coderd import ( - "database/sql" - "errors" "fmt" "net/http" @@ -118,10 +116,11 @@ func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) { Name: provisionerKey.Name, }) if err != nil { - if errors.Is(err, sql.ErrNoRows) { + if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) return } + httpapi.InternalServerError(rw, err) return } diff --git a/enterprise/coderd/provisionerkeys_test.go b/enterprise/coderd/provisionerkeys_test.go index f40d4ff2951df..4c9408e0a27de 100644 --- a/enterprise/coderd/provisionerkeys_test.go +++ b/enterprise/coderd/provisionerkeys_test.go @@ -77,7 +77,7 @@ func TestProvisionerKeys(t *testing.T) { // org admin can conflict on name creating a provisioner key _, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{ - Name: "key", + Name: "KEY", // still conflicts }) require.ErrorContains(t, err, "already exists in organization") From 1a5deb8e1ba0aacf160e9e3ed41359a26b51ef5e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 16 Jul 2024 16:07:56 +0000 Subject: [PATCH 19/20] fix migration --- ...provisioner_keys.down.sql => 000227_provisioner_keys.down.sql} | 0 ...226_provisioner_keys.up.sql => 000227_provisioner_keys.up.sql} | 0 ...226_provisioner_keys.up.sql => 000227_provisioner_keys.up.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000226_provisioner_keys.down.sql => 000227_provisioner_keys.down.sql} (100%) rename coderd/database/migrations/{000226_provisioner_keys.up.sql => 000227_provisioner_keys.up.sql} (100%) rename coderd/database/migrations/testdata/fixtures/{000226_provisioner_keys.up.sql => 000227_provisioner_keys.up.sql} (100%) diff --git a/coderd/database/migrations/000226_provisioner_keys.down.sql b/coderd/database/migrations/000227_provisioner_keys.down.sql similarity index 100% rename from coderd/database/migrations/000226_provisioner_keys.down.sql rename to coderd/database/migrations/000227_provisioner_keys.down.sql diff --git a/coderd/database/migrations/000226_provisioner_keys.up.sql b/coderd/database/migrations/000227_provisioner_keys.up.sql similarity index 100% rename from coderd/database/migrations/000226_provisioner_keys.up.sql rename to coderd/database/migrations/000227_provisioner_keys.up.sql diff --git a/coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql b/coderd/database/migrations/testdata/fixtures/000227_provisioner_keys.up.sql similarity index 100% rename from coderd/database/migrations/testdata/fixtures/000226_provisioner_keys.up.sql rename to coderd/database/migrations/testdata/fixtures/000227_provisioner_keys.up.sql From ff62057b93ebb887e777680ac486ffa66c4ec7e9 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 16 Jul 2024 16:19:15 +0000 Subject: [PATCH 20/20] make gen and test fix --- coderd/database/dbauthz/dbauthz_test.go | 3 ++- coderd/database/dump.sql | 2 +- coderd/database/queries.sql.go | 2 +- coderd/database/unique_constraint.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 22afeb2e67552..a7dc07367dd8f 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "reflect" + "strings" "testing" "time" @@ -1807,7 +1808,7 @@ func (s *MethodTestSuite) TestProvisionerKeys() { ID: uuid.New(), CreatedAt: time.Now(), OrganizationID: org.ID, - Name: coderdtest.RandomName(s.T()), + Name: strings.ToLower(coderdtest.RandomName(s.T())), HashedSecret: []byte(coderdtest.RandomName(s.T())), } //nolint:gosimple // casting is not a simplification diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c7f3f52e91033..d07519cff7de0 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1754,7 +1754,7 @@ CREATE INDEX provisioner_job_logs_id_job_id_idx ON provisioner_job_logs USING bt CREATE INDEX provisioner_jobs_started_at_idx ON provisioner_jobs USING btree (started_at) WHERE (started_at IS NULL); -CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, name); +CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text)); CREATE INDEX template_usage_stats_start_time_idx ON template_usage_stats USING btree (start_time DESC); diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 55930cdc39ebd..cd23525773047 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5497,7 +5497,7 @@ FROM WHERE organization_id = $1 AND - name = lower($2) + lower(name) = lower($2) ` type GetProvisionerKeyByNameParams struct { diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 4fc6d15243fc8..aecae02d572ff 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -88,7 +88,7 @@ const ( UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); - UniqueProvisionerKeysOrganizationIDNameIndex UniqueConstraint = "provisioner_keys_organization_id_name_idx" // CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, name); + UniqueProvisionerKeysOrganizationIDNameIndex UniqueConstraint = "provisioner_keys_organization_id_name_idx" // CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text)); UniqueTemplateUsageStatsStartTimeTemplateIDUserIDIndex UniqueConstraint = "template_usage_stats_start_time_template_id_user_id_idx" // CREATE UNIQUE INDEX template_usage_stats_start_time_template_id_user_id_idx ON template_usage_stats USING btree (start_time, template_id, user_id); UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false); UniqueUserLinksLinkedIDLoginTypeIndex UniqueConstraint = "user_links_linked_id_login_type_idx" // CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text);