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) | + +
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: