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": { "/organizations/{organization}/templates": {
"get": { "get": {
"security": [ "security": [
@@ -8609,6 +8713,14 @@ const docTemplate = `{
} }
} }
}, },
"codersdk.CreateProvisionerKeyResponse": {
"type": "object",
"properties": {
"key": {
"type": "string"
}
}
},
"codersdk.CreateTemplateRequest": { "codersdk.CreateTemplateRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -10762,6 +10874,26 @@ const docTemplate = `{
"ProvisionerJobUnknown" "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": { "codersdk.ProvisionerLogLevel": {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -10897,6 +11029,7 @@ const docTemplate = `{
"organization", "organization",
"organization_member", "organization_member",
"provisioner_daemon", "provisioner_daemon",
"provisioner_keys",
"replicas", "replicas",
"system", "system",
"tailnet_coordinator", "tailnet_coordinator",
@@ -10924,6 +11057,7 @@ const docTemplate = `{
"ResourceOrganization", "ResourceOrganization",
"ResourceOrganizationMember", "ResourceOrganizationMember",
"ResourceProvisionerDaemon", "ResourceProvisionerDaemon",
"ResourceProvisionerKeys",
"ResourceReplicas", "ResourceReplicas",
"ResourceSystem", "ResourceSystem",
"ResourceTailnetCoordinator", "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": { "/organizations/{organization}/templates": {
"get": { "get": {
"security": [ "security": [
@@ -7661,6 +7755,14 @@
} }
} }
}, },
"codersdk.CreateProvisionerKeyResponse": {
"type": "object",
"properties": {
"key": {
"type": "string"
}
}
},
"codersdk.CreateTemplateRequest": { "codersdk.CreateTemplateRequest": {
"type": "object", "type": "object",
"required": ["name", "template_version_id"], "required": ["name", "template_version_id"],
@@ -9702,6 +9804,26 @@
"ProvisionerJobUnknown" "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": { "codersdk.ProvisionerLogLevel": {
"type": "string", "type": "string",
"enum": ["debug"], "enum": ["debug"],
@@ -9819,6 +9941,7 @@
"organization", "organization",
"organization_member", "organization_member",
"provisioner_daemon", "provisioner_daemon",
"provisioner_keys",
"replicas", "replicas",
"system", "system",
"tailnet_coordinator", "tailnet_coordinator",
@@ -9846,6 +9969,7 @@
"ResourceOrganization", "ResourceOrganization",
"ResourceOrganizationMember", "ResourceOrganizationMember",
"ResourceProvisionerDaemon", "ResourceProvisionerDaemon",
"ResourceProvisionerKeys",
"ResourceReplicas", "ResourceReplicas",
"ResourceSystem", "ResourceSystem",
"ResourceTailnetCoordinator", "ResourceTailnetCoordinator",
+20
View File
@@ -1074,6 +1074,10 @@ func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.Del
}, q.db.DeleteOrganizationMember)(ctx, arg) }, 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 { func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil {
return err return err
@@ -1671,6 +1675,14 @@ func (q *querier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt
return q.db.GetProvisionerJobsCreatedAfter(ctx, 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) { func (q *querier) GetProvisionerLogsAfterID(ctx context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) {
// Authorized read on job lets the actor also read the logs. // Authorized read on job lets the actor also read the logs.
_, err := q.GetProvisionerJobByID(ctx, arg.JobID) _, 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) 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) { func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
return database.Replica{}, err return database.Replica{}, err
@@ -2843,6 +2859,10 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab
return q.db.InsertWorkspaceResourceMetadata(ctx, arg) 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) { func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID) workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID)
if err != nil { if err != nil {
+53
View File
@@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "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() { func (s *MethodTestSuite) TestExtraMethods() {
s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) { s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{ 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 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 { func WorkspaceApp(t testing.TB, db database.Store, orig database.WorkspaceApp) database.WorkspaceApp {
resource, err := db.InsertWorkspaceApp(genCtx, database.InsertWorkspaceAppParams{ resource, err := db.InsertWorkspaceApp(genCtx, database.InsertWorkspaceAppParams{
ID: takeFirst(orig.ID, uuid.New()), ID: takeFirst(orig.ID, uuid.New()),
+96
View File
@@ -168,6 +168,7 @@ type data struct {
provisionerDaemons []database.ProvisionerDaemon provisionerDaemons []database.ProvisionerDaemon
provisionerJobLogs []database.ProvisionerJobLog provisionerJobLogs []database.ProvisionerJobLog
provisionerJobs []database.ProvisionerJob provisionerJobs []database.ProvisionerJob
provisionerKeys []database.ProvisionerKey
replicas []database.Replica replicas []database.Replica
templateVersions []database.TemplateVersionTable templateVersions []database.TemplateVersionTable
templateVersionParameters []database.TemplateVersionParameter templateVersionParameters []database.TemplateVersionParameter
@@ -268,6 +269,13 @@ func validateDatabaseType(args interface{}) error {
return nil 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) { func (*FakeQuerier) Ping(_ context.Context) (time.Duration, error) {
return 0, nil return 0, nil
} }
@@ -1734,6 +1742,20 @@ func (q *FakeQuerier) DeleteOrganizationMember(_ context.Context, arg database.D
return nil 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 { func (q *FakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
@@ -3195,6 +3217,32 @@ func (q *FakeQuerier) GetProvisionerJobsCreatedAfter(_ context.Context, after ti
return jobs, nil 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) { func (q *FakeQuerier) GetProvisionerLogsAfterID(_ context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) {
if err := validateDatabaseType(arg); err != nil { if err := validateDatabaseType(arg); err != nil {
return nil, err return nil, err
@@ -6493,6 +6541,34 @@ func (q *FakeQuerier) InsertProvisionerJobLogs(_ context.Context, arg database.I
return logs, nil 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) { func (q *FakeQuerier) InsertReplica(_ context.Context, arg database.InsertReplicaParams) (database.Replica, error) {
if err := validateDatabaseType(arg); err != nil { if err := validateDatabaseType(arg); err != nil {
return database.Replica{}, err return database.Replica{}, err
@@ -7170,6 +7246,26 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat
return metadata, nil 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) { func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
+35
View File
@@ -326,6 +326,13 @@ func (m metricsStore) DeleteOrganizationMember(ctx context.Context, arg database
return r0 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 { func (m metricsStore) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error {
start := time.Now() start := time.Now()
err := m.s.DeleteReplicasUpdatedBefore(ctx, updatedAt) err := m.s.DeleteReplicasUpdatedBefore(ctx, updatedAt)
@@ -900,6 +907,20 @@ func (m metricsStore) GetProvisionerJobsCreatedAfter(ctx context.Context, create
return jobs, err 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) { func (m metricsStore) GetProvisionerLogsAfterID(ctx context.Context, arg database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) {
start := time.Now() start := time.Now()
logs, err := m.s.GetProvisionerLogsAfterID(ctx, arg) logs, err := m.s.GetProvisionerLogsAfterID(ctx, arg)
@@ -1642,6 +1663,13 @@ func (m metricsStore) InsertProvisionerJobLogs(ctx context.Context, arg database
return logs, err 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) { func (m metricsStore) InsertReplica(ctx context.Context, arg database.InsertReplicaParams) (database.Replica, error) {
start := time.Now() start := time.Now()
replica, err := m.s.InsertReplica(ctx, arg) replica, err := m.s.InsertReplica(ctx, arg)
@@ -1803,6 +1831,13 @@ func (m metricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, arg d
return metadata, err 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) { func (m metricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
start := time.Now() start := time.Now()
r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID) 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) 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. // DeleteReplicasUpdatedBefore mocks base method.
func (m *MockStore) DeleteReplicasUpdatedBefore(arg0 context.Context, arg1 time.Time) error { func (m *MockStore) DeleteReplicasUpdatedBefore(arg0 context.Context, arg1 time.Time) error {
m.ctrl.T.Helper() 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) 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. // GetProvisionerLogsAfterID mocks base method.
func (m *MockStore) GetProvisionerLogsAfterID(arg0 context.Context, arg1 database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) { func (m *MockStore) GetProvisionerLogsAfterID(arg0 context.Context, arg1 database.GetProvisionerLogsAfterIDParams) ([]database.ProvisionerJobLog, error) {
m.ctrl.T.Helper() 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) 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. // InsertReplica mocks base method.
func (m *MockStore) InsertReplica(arg0 context.Context, arg1 database.InsertReplicaParams) (database.Replica, error) { func (m *MockStore) InsertReplica(arg0 context.Context, arg1 database.InsertReplicaParams) (database.Replica, error) {
m.ctrl.T.Helper() 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) 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. // ListWorkspaceAgentPortShares mocks base method.
func (m *MockStore) ListWorkspaceAgentPortShares(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentPortShare, error) { func (m *MockStore) ListWorkspaceAgentPortShares(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
m.ctrl.T.Helper() 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.'; 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 ( CREATE TABLE replicas (
id uuid NOT NULL, id uuid NOT NULL,
created_at timestamp with time zone 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 ALTER TABLE ONLY provisioner_jobs
ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY site_configs
ADD CONSTRAINT site_configs_key_key UNIQUE (key); 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 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); 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).'; 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 ALTER TABLE ONLY provisioner_jobs
ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; 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 ALTER TABLE ONLY tailnet_agents
ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; 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; 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; 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; 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; 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; 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; 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) 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 { func (w WorkspaceProxy) RBACObject() rbac.Object {
return rbac.ResourceWorkspaceProxy. return rbac.ResourceWorkspaceProxy.
WithID(w.ID) WithID(w.ID)
+8
View File
@@ -2185,6 +2185,14 @@ type ProvisionerJobLog struct {
ID int64 `db:"id" json:"id"` 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 { type Replica struct {
ID uuid.UUID `db:"id" json:"id"` ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"` CreatedAt time.Time `db:"created_at" json:"created_at"`
+5
View File
@@ -92,6 +92,7 @@ type sqlcQuerier interface {
DeleteOldWorkspaceAgentStats(ctx context.Context) error DeleteOldWorkspaceAgentStats(ctx context.Context) error
DeleteOrganization(ctx context.Context, id uuid.UUID) error DeleteOrganization(ctx context.Context, id uuid.UUID) error
DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error
DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error)
DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, 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) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error)
GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, ids []uuid.UUID) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error)
GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, 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) GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error)
GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error)
GetQuotaConsumedForUser(ctx context.Context, ownerID 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) InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error)
InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, 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) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error)
InsertTemplate(ctx context.Context, arg InsertTemplateParams) error InsertTemplate(ctx context.Context, arg InsertTemplateParams) error
InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error
@@ -370,6 +374,7 @@ type sqlcQuerier interface {
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, 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) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error)
// Arguments are optional with uuid.Nil to ignore. // Arguments are optional with uuid.Nil to ignore.
// - Use just 'organization_id' to get all members of an org // - 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 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 const getWorkspaceProxies = `-- name: GetWorkspaceProxies :many
SELECT SELECT
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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", 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 // ResourceReplicas
// Valid Actions // Valid Actions
// - "ActionRead" :: read replicas // - "ActionRead" :: read replicas
@@ -269,6 +278,7 @@ func AllResources() []Objecter {
ResourceOrganization, ResourceOrganization,
ResourceOrganizationMember, ResourceOrganizationMember,
ResourceProvisionerDaemon, ResourceProvisionerDaemon,
ResourceProvisionerKeys,
ResourceReplicas, ResourceReplicas,
ResourceSystem, ResourceSystem,
ResourceTailnetCoordinator, ResourceTailnetCoordinator,
+7
View File
@@ -160,6 +160,13 @@ var RBACPermissions = map[string]PermissionDefinition{
ActionDelete: actDef("delete a provisioner daemon"), 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": { "organization": {
Actions: map[Action]ActionDefinition{ Actions: map[Action]ActionDefinition{
ActionCreate: actDef("create an organization"), ActionCreate: actDef("create an organization"),
+9
View File
@@ -488,6 +488,15 @@ func TestRolePermissions(t *testing.T) {
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, 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", Name: "System",
Actions: crud, Actions: crud,
+2
View File
@@ -56,6 +56,7 @@ const (
FeatureAccessControl FeatureName = "access_control" FeatureAccessControl FeatureName = "access_control"
FeatureControlSharedPorts FeatureName = "control_shared_ports" FeatureControlSharedPorts FeatureName = "control_shared_ports"
FeatureCustomRoles FeatureName = "custom_roles" FeatureCustomRoles FeatureName = "custom_roles"
FeatureMultipleOrganizations FeatureName = "multiple_organizations"
) )
// FeatureNames must be kept in-sync with the Feature enum above. // FeatureNames must be kept in-sync with the Feature enum above.
@@ -77,6 +78,7 @@ var FeatureNames = []FeatureName{
FeatureAccessControl, FeatureAccessControl,
FeatureControlSharedPorts, FeatureControlSharedPorts,
FeatureCustomRoles, FeatureCustomRoles,
FeatureMultipleOrganizations,
} }
// Humanize returns the feature name in a human-readable format. // 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 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" ResourceOrganization RBACResource = "organization"
ResourceOrganizationMember RBACResource = "organization_member" ResourceOrganizationMember RBACResource = "organization_member"
ResourceProvisionerDaemon RBACResource = "provisioner_daemon" ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
ResourceProvisionerKeys RBACResource = "provisioner_keys"
ResourceReplicas RBACResource = "replicas" ResourceReplicas RBACResource = "replicas"
ResourceSystem RBACResource = "system" ResourceSystem RBACResource = "system"
ResourceTailnetCoordinator RBACResource = "tailnet_coordinator" ResourceTailnetCoordinator RBACResource = "tailnet_coordinator"
@@ -69,6 +70,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceProvisionerKeys: {ActionCreate, ActionDelete, ActionRead},
ResourceReplicas: {ActionRead}, ResourceReplicas: {ActionRead},
ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceTailnetCoordinator: {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). 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 ## Get active replicas
### Code samples ### Code samples
+3
View File
@@ -182,6 +182,7 @@ Status Code **200**
| `resource_type` | `organization` | | `resource_type` | `organization` |
| `resource_type` | `organization_member` | | `resource_type` | `organization_member` |
| `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_keys` |
| `resource_type` | `replicas` | | `resource_type` | `replicas` |
| `resource_type` | `system` | | `resource_type` | `system` |
| `resource_type` | `tailnet_coordinator` | | `resource_type` | `tailnet_coordinator` |
@@ -304,6 +305,7 @@ Status Code **200**
| `resource_type` | `organization` | | `resource_type` | `organization` |
| `resource_type` | `organization_member` | | `resource_type` | `organization_member` |
| `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_keys` |
| `resource_type` | `replicas` | | `resource_type` | `replicas` |
| `resource_type` | `system` | | `resource_type` | `system` |
| `resource_type` | `tailnet_coordinator` | | `resource_type` | `tailnet_coordinator` |
@@ -578,6 +580,7 @@ Status Code **200**
| `resource_type` | `organization` | | `resource_type` | `organization` |
| `resource_type` | `organization_member` | | `resource_type` | `organization_member` |
| `resource_type` | `provisioner_daemon` | | `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_keys` |
| `resource_type` | `replicas` | | `resource_type` | `replicas` |
| `resource_type` | `system` | | `resource_type` | `system` |
| `resource_type` | `tailnet_coordinator` | | `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 | | | | `icon` | string | false | | |
| `name` | string | true | | | | `name` | string | true | | |
## codersdk.CreateProvisionerKeyResponse
```json
{
"key": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ----- | ------ | -------- | ------------ | ----------- |
| `key` | string | false | | |
## codersdk.CreateTemplateRequest ## codersdk.CreateTemplateRequest
```json ```json
@@ -3810,6 +3824,26 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `failed` | | `failed` |
| `unknown` | | `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 ## codersdk.ProvisionerLogLevel
```json ```json
@@ -3958,6 +3992,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `organization` | | `organization` |
| `organization_member` | | `organization_member` |
| `provisioner_daemon` | | `provisioner_daemon` |
| `provisioner_keys` |
| `replicas` | | `replicas` |
| `system` | | `system` |
| `tailnet_coordinator` | | `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.Route("/workspaceproxies", func(r chi.Router) {
r.Use( r.Use(
api.moonsEnabledMW, api.RequireFeatureMW(codersdk.FeatureWorkspaceProxy),
) )
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use( r.Use(
@@ -254,6 +254,22 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Get("/", api.groupByOrganization) 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 // 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 // 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 // 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.FeatureUserRoleManagement: true,
codersdk.FeatureAccessControl: true, codersdk.FeatureAccessControl: true,
codersdk.FeatureControlSharedPorts: true, codersdk.FeatureControlSharedPorts: true,
codersdk.FeatureMultipleOrganizations: true,
}) })
if err != nil { if err != nil {
return err return err
@@ -751,6 +768,11 @@ func (api *API) updateEntitlements(ctx context.Context) error {
api.AGPL.CustomRoleHandler.Store(&handler) 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 // External token encryption is soft-enforced
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption] featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0 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 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 { func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
api.entitlementsMu.RLock() api.entitlementsMu.RLock()
@@ -343,19 +343,21 @@ func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler {
}) })
} }
func (api *API) moonsEnabledMW(next http.Handler) http.Handler { 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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Entitlement must be enabled. // Entitlement must be enabled.
api.entitlementsMu.RLock() api.entitlementsMu.RLock()
proxy := api.entitlements.Features[codersdk.FeatureWorkspaceProxy].Enabled enabled := api.entitlements.Features[feat].Enabled
api.entitlementsMu.RUnlock() api.entitlementsMu.RUnlock()
if !proxy { if !enabled {
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{ 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 return
} }
next.ServeHTTP(rw, r) next.ServeHTTP(rw, r)
}) })
}
} }
+22
View File
@@ -233,6 +233,16 @@ export interface CreateOrganizationRequest {
readonly icon?: string; 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 // From codersdk/organizations.go
export interface CreateTemplateRequest { export interface CreateTemplateRequest {
readonly name: string; readonly name: string;
@@ -955,6 +965,14 @@ export interface ProvisionerJobLog {
readonly output: string; 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 // From codersdk/workspaceproxy.go
export interface ProxyHealthReport { export interface ProxyHealthReport {
readonly errors: readonly string[]; readonly errors: readonly string[];
@@ -2029,6 +2047,7 @@ export type FeatureName =
| "external_token_encryption" | "external_token_encryption"
| "high_availability" | "high_availability"
| "multiple_external_auth" | "multiple_external_auth"
| "multiple_organizations"
| "scim" | "scim"
| "template_rbac" | "template_rbac"
| "user_limit" | "user_limit"
@@ -2047,6 +2066,7 @@ export const FeatureNames: FeatureName[] = [
"external_token_encryption", "external_token_encryption",
"high_availability", "high_availability",
"multiple_external_auth", "multiple_external_auth",
"multiple_organizations",
"scim", "scim",
"template_rbac", "template_rbac",
"user_limit", "user_limit",
@@ -2206,6 +2226,7 @@ export type RBACResource =
| "organization" | "organization"
| "organization_member" | "organization_member"
| "provisioner_daemon" | "provisioner_daemon"
| "provisioner_keys"
| "replicas" | "replicas"
| "system" | "system"
| "tailnet_coordinator" | "tailnet_coordinator"
@@ -2232,6 +2253,7 @@ export const RBACResources: RBACResource[] = [
"organization", "organization",
"organization_member", "organization_member",
"provisioner_daemon", "provisioner_daemon",
"provisioner_keys",
"replicas", "replicas",
"system", "system",
"tailnet_coordinator", "tailnet_coordinator",