chore: add provisioner key crud apis (#13857)

This commit is contained in:
Garrett Delfosse
2024-07-16 13:27:12 -04:00
committed by GitHub
parent a5e4bf38fe
commit b697c6939a
35 changed files with 1447 additions and 16 deletions
+134
View File
@@ -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",
+124
View File
@@ -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",
+20
View File
@@ -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 {
+53
View File
@@ -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{
+12
View File
@@ -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()),
+96
View File
@@ -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()
+35
View File
@@ -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)
+74
View File
@@ -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()
+16
View File
@@ -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;
@@ -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;
@@ -0,0 +1 @@
DROP TABLE provisioner_keys;
@@ -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));
@@ -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);
+6
View File
@@ -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)
+8
View File
@@ -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"`
+5
View File
@@ -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
+141
View File
@@ -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
@@ -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;
+2
View File
@@ -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);
+58
View File
@@ -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))
})
}
}
+31
View File
@@ -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
}
+10
View File
@@ -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,
+7
View File
@@ -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"),
+9
View File
@@ -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,
+2
View File
@@ -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.
+69
View File
@@ -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
}
+2
View File
@@ -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},
+118
View File
@@ -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) |
<h3 id="list-provisioner-key-responseschema">Response Schema</h3>
Status Code **200**
| Name | Type | Required | Restrictions | Description |
| ---------------- | ----------------- | -------- | ------------ | ----------- |
| `[array item]` | array | false | | |
| `» created_at` | string(date-time) | false | | |
| `» id` | string(uuid) | false | | |
| `» name` | string | false | | |
| `» organization` | string(uuid) | false | | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Create provisioner key
### Code samples
```shell
# Example request using curl
curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/provisionerkeys \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`POST /organizations/{organization}/provisionerkeys`
### Parameters
| Name | In | Type | Required | Description |
| -------------- | ---- | ------ | -------- | --------------- |
| `organization` | path | string | true | Organization ID |
### Example responses
> 201 Response
```json
{
"key": "string"
}
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------- |
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.CreateProvisionerKeyResponse](schemas.md#codersdkcreateprovisionerkeyresponse) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Delete provisioner key
### Code samples
```shell
# Example request using curl
curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization}/provisionerkeys/{provisionerkey} \
-H 'Coder-Session-Token: API_KEY'
```
`DELETE /organizations/{organization}/provisionerkeys/{provisionerkey}`
### Parameters
| Name | In | Type | Required | Description |
| ---------------- | ---- | ------ | -------- | -------------------- |
| `organization` | path | string | true | Organization ID |
| `provisionerkey` | path | string | true | Provisioner key name |
### Responses
| Status | Meaning | Description | Schema |
| ------ | --------------------------------------------------------------- | ----------- | ------ |
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get active replicas
### Code samples
+3
View File
@@ -182,6 +182,7 @@ Status Code **200**
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_keys` |
| `resource_type` | `replicas` |
| `resource_type` | `system` |
| `resource_type` | `tailnet_coordinator` |
@@ -304,6 +305,7 @@ Status Code **200**
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_keys` |
| `resource_type` | `replicas` |
| `resource_type` | `system` |
| `resource_type` | `tailnet_coordinator` |
@@ -578,6 +580,7 @@ Status Code **200**
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_keys` |
| `resource_type` | `replicas` |
| `resource_type` | `system` |
| `resource_type` | `tailnet_coordinator` |
+35
View File
@@ -1047,6 +1047,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `icon` | string | false | | |
| `name` | string | true | | |
## codersdk.CreateProvisionerKeyResponse
```json
{
"key": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ----- | ------ | -------- | ------------ | ----------- |
| `key` | string | false | | |
## codersdk.CreateTemplateRequest
```json
@@ -3810,6 +3824,26 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `failed` |
| `unknown` |
## codersdk.ProvisionerKey
```json
{
"created_at": "2019-08-24T14:15:22Z",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"organization": "452c1a86-a0af-475b-b03f-724878b0f387"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| -------------- | ------ | -------- | ------------ | ----------- |
| `created_at` | string | false | | |
| `id` | string | false | | |
| `name` | string | false | | |
| `organization` | string | false | | |
## codersdk.ProvisionerLogLevel
```json
@@ -3958,6 +3992,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `organization` |
| `organization_member` |
| `provisioner_daemon` |
| `provisioner_keys` |
| `replicas` |
| `system` |
| `tailnet_coordinator` |
+23 -1
View File
@@ -205,7 +205,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
})
r.Route("/workspaceproxies", func(r chi.Router) {
r.Use(
api.moonsEnabledMW,
api.RequireFeatureMW(codersdk.FeatureWorkspaceProxy),
)
r.Group(func(r chi.Router) {
r.Use(
@@ -254,6 +254,22 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Get("/", api.groupByOrganization)
})
})
r.Route("/organizations/{organization}/provisionerkeys", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(api.Database),
api.RequireFeatureMW(codersdk.FeatureMultipleOrganizations),
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentMultiOrganization),
)
r.Get("/", api.provisionerKeys)
r.Post("/", api.postProvisionerKey)
r.Route("/{provisionerkey}", func(r chi.Router) {
r.Use(
httpmw.ExtractProvisionerKeyParam(options.Database),
)
r.Delete("/", api.deleteProvisionerKey)
})
})
// TODO: provisioner daemons are not scoped to organizations in the database, so placing them
// under an organization route doesn't make sense. In order to allow the /serve endpoint to
// work with a pre-shared key (PSK) without an API key, these routes will simply ignore the
@@ -566,6 +582,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
codersdk.FeatureUserRoleManagement: true,
codersdk.FeatureAccessControl: true,
codersdk.FeatureControlSharedPorts: true,
codersdk.FeatureMultipleOrganizations: true,
})
if err != nil {
return err
@@ -751,6 +768,11 @@ func (api *API) updateEntitlements(ctx context.Context) error {
api.AGPL.CustomRoleHandler.Store(&handler)
}
if initial, changed, enabled := featureChanged(codersdk.FeatureMultipleOrganizations); shouldUpdate(initial, changed, enabled) {
var handler coderd.CustomRoleHandler = &enterpriseCustomRoleHandler{API: api, Enabled: enabled}
api.AGPL.CustomRoleHandler.Store(&handler)
}
// External token encryption is soft-enforced
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
+149
View File
@@ -0,0 +1,149 @@
package coderd
import (
"fmt"
"net/http"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/provisionerkey"
"github.com/coder/coder/v2/codersdk"
)
// @Summary Create provisioner key
// @ID create-provisioner-key
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param organization path string true "Organization ID"
// @Success 201 {object} codersdk.CreateProvisionerKeyResponse
// @Router /organizations/{organization}/provisionerkeys [post]
func (api *API) postProvisionerKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
var req codersdk.CreateProvisionerKeyRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if req.Name == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Name is required",
Validations: []codersdk.ValidationError{
{
Field: "name",
Detail: "Name is required",
},
},
})
return
}
if len(req.Name) > 64 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Name must be at most 64 characters",
Validations: []codersdk.ValidationError{
{
Field: "name",
Detail: "Name must be at most 64 characters",
},
},
})
return
}
params, token, err := provisionerkey.New(organization.ID, req.Name)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
_, err = api.Database.InsertProvisionerKey(ctx, params)
if database.IsUniqueViolation(err, database.UniqueProvisionerKeysOrganizationIDNameIndex) {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("Provisioner key with name '%s' already exists in organization", req.Name),
})
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.CreateProvisionerKeyResponse{
Key: token,
})
}
// @Summary List provisioner key
// @ID list-provisioner-key
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param organization path string true "Organization ID"
// @Success 200 {object} []codersdk.ProvisionerKey
// @Router /organizations/{organization}/provisionerkeys [get]
func (api *API) provisionerKeys(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
pks, err := api.Database.ListProvisionerKeysByOrganization(ctx, organization.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerKeys(pks))
}
// @Summary Delete provisioner key
// @ID delete-provisioner-key
// @Security CoderSessionToken
// @Tags Enterprise
// @Param organization path string true "Organization ID"
// @Param provisionerkey path string true "Provisioner key name"
// @Success 204
// @Router /organizations/{organization}/provisionerkeys/{provisionerkey} [delete]
func (api *API) deleteProvisionerKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
provisionerKey := httpmw.ProvisionerKeyParam(r)
pk, err := api.Database.GetProvisionerKeyByName(ctx, database.GetProvisionerKeyByNameParams{
OrganizationID: organization.ID,
Name: provisionerKey.Name,
})
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.InternalServerError(rw, err)
return
}
err = api.Database.DeleteProvisionerKey(ctx, pk.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
}
func convertProvisionerKeys(dbKeys []database.ProvisionerKey) []codersdk.ProvisionerKey {
keys := make([]codersdk.ProvisionerKey, 0, len(dbKeys))
for _, dbKey := range dbKeys {
keys = append(keys, codersdk.ProvisionerKey{
ID: dbKey.ID,
CreatedAt: dbKey.CreatedAt,
OrganizationID: dbKey.OrganizationID,
Name: dbKey.Name,
// HashedSecret - never include the access token in the API response
})
}
return keys
}
+108
View File
@@ -0,0 +1,108 @@
package coderd_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
func TestProvisionerKeys(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*10)
t.Cleanup(cancel)
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
client, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
otherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
outsideOrgAdmin, _ := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.ScopedRoleOrgAdmin(otherOrg.ID))
// member cannot create a provisioner key
_, err := member.CreateProvisionerKey(ctx, otherOrg.ID, codersdk.CreateProvisionerKeyRequest{
Name: "key",
})
require.ErrorContains(t, err, "Resource not found")
// member cannot list provisioner keys
_, err = member.ListProvisionerKeys(ctx, otherOrg.ID)
require.ErrorContains(t, err, "Resource not found")
// member cannot delete a provisioner key
err = member.DeleteProvisionerKey(ctx, otherOrg.ID, "key")
require.ErrorContains(t, err, "Resource not found")
// outside org admin cannot create a provisioner key
_, err = outsideOrgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "key",
})
require.ErrorContains(t, err, "Resource not found")
// outside org admin cannot list provisioner keys
_, err = outsideOrgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID)
require.ErrorContains(t, err, "Resource not found")
// outside org admin cannot delete a provisioner key
err = outsideOrgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key")
require.ErrorContains(t, err, "Resource not found")
// org admin can list provisioner keys and get an empty list
keys, err := orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID)
require.NoError(t, err, "org admin list provisioner keys")
require.Len(t, keys, 0, "org admin list provisioner keys")
// org admin can create a provisioner key
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "Key", // case insensitive
})
require.NoError(t, err, "org admin create provisioner key")
// org admin can conflict on name creating a provisioner key
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "KEY", // still conflicts
})
require.ErrorContains(t, err, "already exists in organization")
// key name cannot be too long
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "Everyone please pass your watermelons to the front of the pool, the storm is approaching.",
})
require.ErrorContains(t, err, "must be at most 64 characters")
// key name cannot be empty
_, err = orgAdmin.CreateProvisionerKey(ctx, owner.OrganizationID, codersdk.CreateProvisionerKeyRequest{
Name: "",
})
require.ErrorContains(t, err, "is required")
// org admin can list provisioner keys
keys, err = orgAdmin.ListProvisionerKeys(ctx, owner.OrganizationID)
require.NoError(t, err, "org admin list provisioner keys")
require.Len(t, keys, 1, "org admin list provisioner keys")
// org admin can delete a provisioner key
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key") // using lowercase here works
require.NoError(t, err, "org admin delete provisioner key")
// org admin cannot delete a provisioner key that doesn't exist
err = orgAdmin.DeleteProvisionerKey(ctx, owner.OrganizationID, "key")
require.ErrorContains(t, err, "Resource not found")
}
+7 -5
View File
@@ -327,7 +327,7 @@ func convertSDKTemplateRole(role codersdk.TemplateRole) []policy.Action {
return nil
}
// TODO reduce the duplication across all of these.
// TODO move to api.RequireFeatureMW when we are OK with changing the behavior.
func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
api.entitlementsMu.RLock()
@@ -343,15 +343,16 @@ func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler {
})
}
func (api *API) moonsEnabledMW(next http.Handler) http.Handler {
func (api *API) RequireFeatureMW(feat codersdk.FeatureName) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Entitlement must be enabled.
api.entitlementsMu.RLock()
proxy := api.entitlements.Features[codersdk.FeatureWorkspaceProxy].Enabled
enabled := api.entitlements.Features[feat].Enabled
api.entitlementsMu.RUnlock()
if !proxy {
if !enabled {
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
Message: "External workspace proxies is an Enterprise feature. Contact sales!",
Message: fmt.Sprintf("%s is an Enterprise feature. Contact sales!", feat.Humanize()),
})
return
}
@@ -359,3 +360,4 @@ func (api *API) moonsEnabledMW(next http.Handler) http.Handler {
next.ServeHTTP(rw, r)
})
}
}
+22
View File
@@ -233,6 +233,16 @@ export interface CreateOrganizationRequest {
readonly icon?: string;
}
// From codersdk/provisionerdaemons.go
export interface CreateProvisionerKeyRequest {
readonly name: string;
}
// From codersdk/provisionerdaemons.go
export interface CreateProvisionerKeyResponse {
readonly key: string;
}
// From codersdk/organizations.go
export interface CreateTemplateRequest {
readonly name: string;
@@ -955,6 +965,14 @@ export interface ProvisionerJobLog {
readonly output: string;
}
// From codersdk/provisionerdaemons.go
export interface ProvisionerKey {
readonly id: string;
readonly created_at: string;
readonly organization: string;
readonly name: string;
}
// From codersdk/workspaceproxy.go
export interface ProxyHealthReport {
readonly errors: readonly string[];
@@ -2029,6 +2047,7 @@ export type FeatureName =
| "external_token_encryption"
| "high_availability"
| "multiple_external_auth"
| "multiple_organizations"
| "scim"
| "template_rbac"
| "user_limit"
@@ -2047,6 +2066,7 @@ export const FeatureNames: FeatureName[] = [
"external_token_encryption",
"high_availability",
"multiple_external_auth",
"multiple_organizations",
"scim",
"template_rbac",
"user_limit",
@@ -2206,6 +2226,7 @@ export type RBACResource =
| "organization"
| "organization_member"
| "provisioner_daemon"
| "provisioner_keys"
| "replicas"
| "system"
| "tailnet_coordinator"
@@ -2232,6 +2253,7 @@ export const RBACResources: RBACResource[] = [
"organization",
"organization_member",
"provisioner_daemon",
"provisioner_keys",
"replicas",
"system",
"tailnet_coordinator",