Skip to content

Commit 2279441

Browse files
authored
feat: add --key flag to provisionerd start (coder#14002)
1 parent c082868 commit 2279441

File tree

4 files changed

+225
-36
lines changed

4 files changed

+225
-36
lines changed

enterprise/cli/provisionerdaemonstart.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/coder/coder/v2/cli/cliui"
2525
"github.com/coder/coder/v2/cli/cliutil"
2626
"github.com/coder/coder/v2/coderd/database"
27+
"github.com/coder/coder/v2/coderd/provisionerkey"
2728
"github.com/coder/coder/v2/codersdk"
2829
"github.com/coder/coder/v2/codersdk/drpc"
2930
"github.com/coder/coder/v2/provisioner/terraform"
@@ -46,6 +47,7 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
4647
pollInterval time.Duration
4748
pollJitter time.Duration
4849
preSharedKey string
50+
provisionerKey string
4951
verbose bool
5052

5153
prometheusEnable bool
@@ -83,8 +85,8 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
8385
return xerrors.Errorf("current organization: %w", err)
8486
}
8587

86-
if preSharedKey == "" {
87-
return xerrors.New("must provide a pre-shared key when not authenticated as a user")
88+
if preSharedKey == "" && provisionerKey == "" {
89+
return xerrors.New("must provide a pre-shared key or provisioner key when not authenticated as a user")
8890
}
8991

9092
org = codersdk.Organization{MinimalOrganization: codersdk.MinimalOrganization{ID: uuid.Nil}}
@@ -113,6 +115,19 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
113115
return err
114116
}
115117

118+
if provisionerKey != "" {
119+
if preSharedKey != "" {
120+
return xerrors.New("cannot provide both provisioner key --key and pre-shared key --psk")
121+
}
122+
if len(rawTags) > 0 {
123+
return xerrors.New("cannot provide tags when using provisioner key")
124+
}
125+
_, _, err := provisionerkey.Parse(provisionerKey)
126+
if err != nil {
127+
return xerrors.Errorf("parse provisioner key: %w", err)
128+
}
129+
}
130+
116131
logOpts := []clilog.Option{
117132
clilog.WithFilter(logFilter...),
118133
clilog.WithHuman(logHuman),
@@ -136,12 +151,17 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
136151
logger.Info(ctx, "note: untagged provisioners can only pick up jobs from untagged templates")
137152
}
138153

139-
// When authorizing with a PSK, we automatically scope the provisionerd
140-
// to organization. Scoping to user with PSK auth is not a valid configuration.
154+
// When authorizing with a PSK / provisioner key, we automatically scope the provisionerd
155+
// to organization. Scoping to user with PSK / provisioner key auth is not a valid configuration.
141156
if preSharedKey != "" {
142-
logger.Info(ctx, "psk auth automatically sets tag "+provisionersdk.TagScope+"="+provisionersdk.ScopeOrganization)
157+
logger.Info(ctx, "psk automatically sets tag "+provisionersdk.TagScope+"="+provisionersdk.ScopeOrganization)
143158
tags[provisionersdk.TagScope] = provisionersdk.ScopeOrganization
144159
}
160+
if provisionerKey != "" {
161+
logger.Info(ctx, "provisioner key auth automatically sets tag "+provisionersdk.TagScope+" empty")
162+
// no scope tag will default to org scope
163+
delete(tags, provisionersdk.TagScope)
164+
}
145165

146166
err = os.MkdirAll(cacheDir, 0o700)
147167
if err != nil {
@@ -210,9 +230,10 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
210230
Provisioners: []codersdk.ProvisionerType{
211231
codersdk.ProvisionerTypeTerraform,
212232
},
213-
Tags: tags,
214-
PreSharedKey: preSharedKey,
215-
Organization: org.ID,
233+
Tags: tags,
234+
PreSharedKey: preSharedKey,
235+
Organization: org.ID,
236+
ProvisionerKey: provisionerKey,
216237
})
217238
}, &provisionerd.Options{
218239
Logger: logger,
@@ -296,6 +317,13 @@ func (r *RootCmd) provisionerDaemonStart() *serpent.Command {
296317
Description: "Pre-shared key to authenticate with Coder server.",
297318
Value: serpent.StringOf(&preSharedKey),
298319
},
320+
{
321+
Flag: "key",
322+
Env: "CODER_PROVISIONER_DAEMON_KEY",
323+
Description: "Provisioner key to authenticate with Coder server.",
324+
Value: serpent.StringOf(&provisionerKey),
325+
Hidden: true,
326+
},
299327
{
300328
Flag: "name",
301329
Env: "CODER_PROVISIONER_DAEMON_NAME",

enterprise/cli/provisionerdaemonstart_test.go

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func TestProvisionerDaemon_PSK(t *testing.T) {
153153
ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
154154
defer cancel()
155155
err = inv.WithContext(ctx).Run()
156-
require.ErrorContains(t, err, "must provide a pre-shared key when not authenticated as a user")
156+
require.ErrorContains(t, err, "must provide a pre-shared key or provisioner key when not authenticated as a user")
157157
})
158158
}
159159

@@ -301,6 +301,165 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) {
301301
})
302302
}
303303

304+
func TestProvisionerDaemon_ProvisionerKey(t *testing.T) {
305+
t.Parallel()
306+
307+
t.Run("OK", func(t *testing.T) {
308+
t.Parallel()
309+
310+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
311+
defer cancel()
312+
dv := coderdtest.DeploymentValues(t)
313+
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
314+
client, user := coderdenttest.New(t, &coderdenttest.Options{
315+
ProvisionerDaemonPSK: "provisionersftw",
316+
LicenseOptions: &coderdenttest.LicenseOptions{
317+
Features: license.Features{
318+
codersdk.FeatureExternalProvisionerDaemons: 1,
319+
codersdk.FeatureMultipleOrganizations: 1,
320+
},
321+
},
322+
Options: &coderdtest.Options{
323+
DeploymentValues: dv,
324+
},
325+
})
326+
// nolint:gocritic // test
327+
res, err := client.CreateProvisionerKey(ctx, user.OrganizationID, codersdk.CreateProvisionerKeyRequest{
328+
Name: "dont-TEST-me",
329+
})
330+
require.NoError(t, err)
331+
inv, conf := newCLI(t, "provisionerd", "start", "--key", res.Key, "--name=matt-daemon")
332+
err = conf.URL().Write(client.URL.String())
333+
require.NoError(t, err)
334+
pty := ptytest.New(t).Attach(inv)
335+
clitest.Start(t, inv)
336+
pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon")
337+
pty.ExpectMatchContext(ctx, "matt-daemon")
338+
339+
var daemons []codersdk.ProvisionerDaemon
340+
require.Eventually(t, func() bool {
341+
daemons, err = client.OrganizationProvisionerDaemons(ctx, user.OrganizationID)
342+
if err != nil {
343+
return false
344+
}
345+
return len(daemons) == 1
346+
}, testutil.WaitShort, testutil.IntervalSlow)
347+
require.Equal(t, "matt-daemon", daemons[0].Name)
348+
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
349+
require.Equal(t, buildinfo.Version(), daemons[0].Version)
350+
require.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
351+
})
352+
353+
t.Run("NoPSK", func(t *testing.T) {
354+
t.Parallel()
355+
356+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
357+
defer cancel()
358+
dv := coderdtest.DeploymentValues(t)
359+
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
360+
client, user := coderdenttest.New(t, &coderdenttest.Options{
361+
ProvisionerDaemonPSK: "provisionersftw",
362+
LicenseOptions: &coderdenttest.LicenseOptions{
363+
Features: license.Features{
364+
codersdk.FeatureExternalProvisionerDaemons: 1,
365+
codersdk.FeatureMultipleOrganizations: 1,
366+
},
367+
},
368+
Options: &coderdtest.Options{
369+
DeploymentValues: dv,
370+
},
371+
})
372+
// nolint:gocritic // test
373+
res, err := client.CreateProvisionerKey(ctx, user.OrganizationID, codersdk.CreateProvisionerKeyRequest{
374+
Name: "dont-TEST-me",
375+
})
376+
require.NoError(t, err)
377+
inv, conf := newCLI(t, "provisionerd", "start", "--psk", "provisionersftw", "--key", res.Key, "--name=matt-daemon")
378+
err = conf.URL().Write(client.URL.String())
379+
require.NoError(t, err)
380+
err = inv.WithContext(ctx).Run()
381+
require.ErrorContains(t, err, "cannot provide both provisioner key --key and pre-shared key --psk")
382+
})
383+
384+
t.Run("NoTags", func(t *testing.T) {
385+
t.Parallel()
386+
387+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
388+
defer cancel()
389+
dv := coderdtest.DeploymentValues(t)
390+
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
391+
client, user := coderdenttest.New(t, &coderdenttest.Options{
392+
ProvisionerDaemonPSK: "provisionersftw",
393+
LicenseOptions: &coderdenttest.LicenseOptions{
394+
Features: license.Features{
395+
codersdk.FeatureExternalProvisionerDaemons: 1,
396+
codersdk.FeatureMultipleOrganizations: 1,
397+
},
398+
},
399+
Options: &coderdtest.Options{
400+
DeploymentValues: dv,
401+
},
402+
})
403+
// nolint:gocritic // test
404+
res, err := client.CreateProvisionerKey(ctx, user.OrganizationID, codersdk.CreateProvisionerKeyRequest{
405+
Name: "dont-TEST-me",
406+
})
407+
require.NoError(t, err)
408+
inv, conf := newCLI(t, "provisionerd", "start", "--tag", "mykey=yourvalue", "--key", res.Key, "--name=matt-daemon")
409+
err = conf.URL().Write(client.URL.String())
410+
require.NoError(t, err)
411+
err = inv.WithContext(ctx).Run()
412+
require.ErrorContains(t, err, "cannot provide tags when using provisioner key")
413+
})
414+
415+
t.Run("AnotherOrg", func(t *testing.T) {
416+
t.Parallel()
417+
418+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
419+
defer cancel()
420+
dv := coderdtest.DeploymentValues(t)
421+
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
422+
client, _ := coderdenttest.New(t, &coderdenttest.Options{
423+
ProvisionerDaemonPSK: "provisionersftw",
424+
LicenseOptions: &coderdenttest.LicenseOptions{
425+
Features: license.Features{
426+
codersdk.FeatureExternalProvisionerDaemons: 1,
427+
codersdk.FeatureMultipleOrganizations: 1,
428+
},
429+
},
430+
Options: &coderdtest.Options{
431+
DeploymentValues: dv,
432+
},
433+
})
434+
anotherOrg := coderdtest.CreateOrganization(t, client, coderdtest.CreateOrganizationOptions{})
435+
// nolint:gocritic // test
436+
res, err := client.CreateProvisionerKey(ctx, anotherOrg.ID, codersdk.CreateProvisionerKeyRequest{
437+
Name: "dont-TEST-me",
438+
})
439+
require.NoError(t, err)
440+
inv, conf := newCLI(t, "provisionerd", "start", "--org", anotherOrg.ID.String(), "--key", res.Key, "--name=matt-daemon")
441+
err = conf.URL().Write(client.URL.String())
442+
require.NoError(t, err)
443+
pty := ptytest.New(t).Attach(inv)
444+
clitest.Start(t, inv)
445+
pty.ExpectNoMatchBefore(ctx, "check entitlement", "starting provisioner daemon")
446+
pty.ExpectMatchContext(ctx, "matt-daemon")
447+
448+
var daemons []codersdk.ProvisionerDaemon
449+
require.Eventually(t, func() bool {
450+
daemons, err = client.OrganizationProvisionerDaemons(ctx, anotherOrg.ID)
451+
if err != nil {
452+
return false
453+
}
454+
return len(daemons) == 1
455+
}, testutil.WaitShort, testutil.IntervalSlow)
456+
require.Equal(t, "matt-daemon", daemons[0].Name)
457+
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
458+
require.Equal(t, buildinfo.Version(), daemons[0].Version)
459+
require.Equal(t, proto.CurrentVersion.String(), daemons[0].APIVersion)
460+
})
461+
}
462+
304463
//nolint:paralleltest,tparallel // Test uses a static port.
305464
func TestProvisionerDaemon_PrometheusEnabled(t *testing.T) {
306465
// Ephemeral ports have a tendency to conflict and fail with `bind: address already in use` error.

enterprise/coderd/provisionerdaemons.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/hashicorp/yamux"
1414
"github.com/moby/moby/pkg/namesgenerator"
1515
"go.opentelemetry.io/otel/trace"
16+
"golang.org/x/exp/maps"
1617
"golang.org/x/xerrors"
1718
"nhooyr.io/websocket"
1819
"storj.io/drpc/drpcmux"
@@ -97,39 +98,43 @@ func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags
9798
return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
9899
}
99100

100-
if apiKeyOK {
101-
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
102-
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
103-
// Any authenticated user can create provisioner daemons scoped
104-
// for jobs that they own,
105-
return tags, nil
101+
// Provisioner Key Auth
102+
if pkOK {
103+
if pk.OrganizationID != orgID {
104+
return nil, xerrors.New("provisioner key unauthorized")
106105
}
107-
ua := httpmw.UserAuthorization(r)
108-
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
109-
if err != nil {
110-
if !provAuth {
111-
return nil, xerrors.New("user unauthorized")
112-
}
113-
114-
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
115-
// This is to preserve backwards compatibility with existing user provisioner daemons.
116-
// If using PSK auth, the daemon is, by definition, scoped to the organization.
117-
tags = provisionersdk.MutateTags(uuid.Nil, tags)
118-
return tags, nil
106+
if tags != nil && !maps.Equal(tags, map[string]string{}) {
107+
return nil, xerrors.New("tags are not allowed when using a provisioner key")
119108
}
120109

121-
// User is allowed to create provisioner daemons
110+
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
111+
// Use the provisioner key tags here.
112+
tags = provisionersdk.MutateTags(uuid.Nil, pk.Tags)
122113
return tags, nil
123114
}
124115

125-
if pkOK {
126-
if pk.OrganizationID != orgID {
127-
return nil, xerrors.New("provisioner key unauthorized")
116+
// User Auth
117+
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
118+
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
119+
// Any authenticated user can create provisioner daemons scoped
120+
// for jobs that they own,
121+
return tags, nil
122+
}
123+
ua := httpmw.UserAuthorization(r)
124+
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
125+
if err != nil {
126+
if !provAuth {
127+
return nil, xerrors.New("user unauthorized")
128128
}
129+
130+
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
131+
// This is to preserve backwards compatibility with existing user provisioner daemons.
132+
// If using PSK auth, the daemon is, by definition, scoped to the organization.
133+
tags = provisionersdk.MutateTags(uuid.Nil, tags)
134+
return tags, nil
129135
}
130136

131-
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
132-
tags = provisionersdk.MutateTags(uuid.Nil, tags)
137+
// User is allowed to create provisioner daemons
133138
return tags, nil
134139
}
135140

enterprise/coderd/provisionerdaemons_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,6 @@ func TestProvisionerDaemonServe(t *testing.T) {
703703
Provisioners: []codersdk.ProvisionerType{
704704
codersdk.ProvisionerTypeEcho,
705705
},
706-
Tags: map[string]string{
707-
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
708-
},
709706
PreSharedKey: tc.requestPSK,
710707
ProvisionerKey: tc.requestProvisionerKey,
711708
})

0 commit comments

Comments
 (0)