diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0aecfdea8ff5e..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": [ @@ -10897,6 +11029,7 @@ const docTemplate = `{ "organization", "organization_member", "provisioner_daemon", + "provisioner_keys", "replicas", "system", "tailnet_coordinator", @@ -10924,6 +11057,7 @@ const docTemplate = `{ "ResourceOrganization", "ResourceOrganizationMember", "ResourceProvisionerDaemon", + "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6d5ac99848dbf..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"], @@ -9819,6 +9941,7 @@ "organization", "organization_member", "provisioner_daemon", + "provisioner_keys", "replicas", "system", "tailnet_coordinator", @@ -9846,6 +9969,7 @@ "ResourceOrganization", "ResourceOrganizationMember", "ResourceProvisionerDaemon", + "ResourceProvisionerKeys", "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 85df46dce620c..9f2de9088b5c5 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1074,6 +1074,10 @@ 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 { + 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 { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err @@ -1671,6 +1675,14 @@ 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) { + return fetch(q.log, q.auth, 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 +2627,10 @@ 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) { + 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) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return database.Replica{}, err @@ -2843,6 +2859,10 @@ 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.ProvisionerKey, error) { + return fetchWithPostFilter(q.auth, policy.ActionRead, 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/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 52d375116e6a3..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" @@ -1800,6 +1801,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: strings.ToLower(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.ProvisionerKey{ + { + 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 9effbe1bb69f2..198b4b4f3b6a9 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 @@ -268,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 } @@ -1734,6 +1742,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 +3217,32 @@ func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after ti return jobs, nil } +func (q *FakeQuerier) GetProvisionerKeyByID(_ 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(_ context.Context, arg database.GetProvisionerKeyByNameParams) (database.ProvisionerKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, key := range q.provisionerKeys { + if strings.EqualFold(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 +6541,34 @@ 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 && strings.EqualFold(key.Name, arg.Name)) { + return database.ProvisionerKey{}, newUniqueConstraintError(database.UniqueProvisionerKeysOrganizationIDNameIndex) + } + } + + //nolint:gosimple + provisionerKey := database.ProvisionerKey{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + OrganizationID: arg.OrganizationID, + Name: strings.ToLower(arg.Name), + HashedSecret: arg.HashedSecret, + } + 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 +7246,26 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat return metadata, nil } +func (q *FakeQuerier) ListProvisionerKeysByOrganization(_ context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + keys := make([]database.ProvisionerKey, 0) + for _, key := range q.provisionerKeys { + if key.OrganizationID == organizationID { + keys = append(keys, database.ProvisionerKey{ + ID: key.ID, + CreatedAt: key.CreatedAt, + OrganizationID: key.OrganizationID, + Name: key.Name, + HashedSecret: key.HashedSecret, + }) + } + } + + 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..f249a78436153 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,20 @@ 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) + 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 +1663,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 +1831,13 @@ func (m metricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, arg d return metadata, err } +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()) + 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..093869e655583 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,36 @@ 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() + 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 +3485,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 +3837,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.ProvisionerKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListProvisionerKeysByOrganization", arg0, arg1) + ret0, _ := ret[0].([]database.ProvisionerKey) + 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..d07519cff7de0 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,9 @@ 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_pkey PRIMARY KEY (id); + ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key); @@ -1743,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, lower((name)::text)); + 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).'; @@ -1867,6 +1880,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/000227_provisioner_keys.down.sql b/coderd/database/migrations/000227_provisioner_keys.down.sql new file mode 100644 index 0000000000000..264b235facff2 --- /dev/null +++ b/coderd/database/migrations/000227_provisioner_keys.down.sql @@ -0,0 +1 @@ +DROP TABLE provisioner_keys; diff --git a/coderd/database/migrations/000227_provisioner_keys.up.sql b/coderd/database/migrations/000227_provisioner_keys.up.sql new file mode 100644 index 0000000000000..44942f729f19b --- /dev/null +++ b/coderd/database/migrations/000227_provisioner_keys.up.sql @@ -0,0 +1,9 @@ +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 +); + +CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower(name)); diff --git a/coderd/database/migrations/testdata/fixtures/000227_provisioner_keys.up.sql b/coderd/database/migrations/testdata/fixtures/000227_provisioner_keys.up.sql new file mode 100644 index 0000000000000..418e519677518 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000227_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', 'bb640d07-ca8a-4869-b6bc-ae61ebb2fda1', 'qua', '\xDEADBEEF'::bytea); 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..2a8153deec2d1 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,8 @@ 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) GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) @@ -346,6 +349,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 +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) ([]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 ee439996e34dd..cd23525773047 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5455,6 +5455,147 @@ 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 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 +FROM + provisioner_keys +WHERE + organization_id = $1 +AND + lower(name) = lower($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, 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"` + 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) { + row := q.db.QueryRowContext(ctx, insertProvisionerKey, + arg.ID, + arg.CreatedAt, + arg.OrganizationID, + arg.HashedSecret, + arg.Name, + ) + 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, hashed_secret +FROM + provisioner_keys +WHERE + organization_id = $1 +` + +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 []ProvisionerKey + for rows.Next() { + var i ProvisionerKey + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.OrganizationID, + &i.Name, + &i.HashedSecret, + ); 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..22e714eca350d --- /dev/null +++ b/coderd/database/queries/provisionerkeys.sql @@ -0,0 +1,43 @@ +-- name: InsertProvisionerKey :one +INSERT INTO + provisioner_keys ( + id, + created_at, + organization_id, + name, + hashed_secret + ) +VALUES + ($1, $2, $3, lower(@name), $4) RETURNING *; + +-- name: GetProvisionerKeyByID :one +SELECT + * +FROM + provisioner_keys +WHERE + id = $1; + +-- name: GetProvisionerKeyByName :one +SELECT + * +FROM + provisioner_keys +WHERE + organization_id = $1 +AND + lower(name) = lower(@name); + +-- name: ListProvisionerKeysByOrganization :many +SELECT + * +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..aecae02d572ff 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -44,6 +44,7 @@ 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); + 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); @@ -87,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, 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); diff --git a/coderd/httpmw/provisionerkey.go b/coderd/httpmw/provisionerkey.go new file mode 100644 index 0000000000000..484200f469422 --- /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(provisionerKeyParamContextKey{}).(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/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/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, 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/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index e26778f940045..605d44d88c071 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -265,3 +265,72 @@ 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"` + // HashedSecret - never include the access token in the API response +} + +type CreateProvisionerKeyRequest struct { + Name string `json:"name"` +} + +type CreateProvisionerKeyResponse struct { + Key string `json:"key"` +} + +// 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.MethodGet, + 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.StatusOK { + 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 +} 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/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) | + +