Skip to content

Commit 596ef99

Browse files
committed
feat: accept provisioner keys for provisioner auth
1 parent b817c86 commit 596ef99

File tree

8 files changed

+175
-57
lines changed

8 files changed

+175
-57
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,34 +158,49 @@ func ActorFromContext(ctx context.Context) (rbac.Subject, bool) {
158158
}
159159

160160
var (
161-
subjectProvisionerd = rbac.Subject{
162-
FriendlyName: "Provisioner Daemon",
163-
ID: uuid.Nil.String(),
164-
Roles: rbac.Roles([]rbac.Role{
165-
{
166-
Identifier: rbac.RoleIdentifier{Name: "provisionerd"},
167-
DisplayName: "Provisioner Daemon",
168-
Site: rbac.Permissions(map[string][]policy.Action{
169-
// TODO: Add ProvisionerJob resource type.
170-
rbac.ResourceFile.Type: {policy.ActionRead},
171-
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
172-
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
173-
// Unsure why provisionerd needs update and read personal
174-
rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
175-
rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop},
176-
rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},
177-
rbac.ResourceApiKey.Type: {policy.WildcardSymbol},
178-
// When org scoped provisioner credentials are implemented,
179-
// this can be reduced to read a specific org.
161+
subjectProvisionerd = func(orgID uuid.UUID) rbac.Subject {
162+
sitePermissions := map[string][]policy.Action{
163+
// TODO: Add ProvisionerJob resource type.
164+
rbac.ResourceFile.Type: {policy.ActionRead},
165+
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
166+
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
167+
// Unsure why provisionerd needs update and read personal
168+
rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
169+
rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop},
170+
rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop},
171+
rbac.ResourceApiKey.Type: {policy.WildcardSymbol},
172+
// When org scoped provisioner credentials are implemented,
173+
// this can be reduced to read a specific org.
174+
rbac.ResourceOrganization.Type: {policy.ActionRead},
175+
rbac.ResourceGroup.Type: {policy.ActionRead},
176+
}
177+
orgPermissions := map[string][]rbac.Permission{}
178+
179+
if orgID != uuid.Nil {
180+
// replace site wide org permissions with org scoped permissions
181+
delete(sitePermissions, rbac.ResourceOrganization.Type)
182+
orgPermissions = map[string][]rbac.Permission{
183+
orgID.String(): rbac.Permissions(map[string][]policy.Action{
180184
rbac.ResourceOrganization.Type: {policy.ActionRead},
181-
rbac.ResourceGroup.Type: {policy.ActionRead},
182185
}),
183-
Org: map[string][]rbac.Permission{},
184-
User: []rbac.Permission{},
185-
},
186-
}),
187-
Scope: rbac.ScopeAll,
188-
}.WithCachedASTValue()
186+
}
187+
}
188+
189+
return rbac.Subject{
190+
FriendlyName: "Provisioner Daemon",
191+
ID: uuid.Nil.String(),
192+
Roles: rbac.Roles([]rbac.Role{
193+
{
194+
Identifier: rbac.RoleIdentifier{Name: "provisionerd"},
195+
DisplayName: "Provisioner Daemon",
196+
Site: rbac.Permissions(sitePermissions),
197+
Org: orgPermissions,
198+
User: []rbac.Permission{},
199+
},
200+
}),
201+
Scope: rbac.ScopeAll,
202+
}.WithCachedASTValue()
203+
}
189204

190205
subjectAutostart = rbac.Subject{
191206
FriendlyName: "Autostart",
@@ -261,7 +276,13 @@ var (
261276
// AsProvisionerd returns a context with an actor that has permissions required
262277
// for provisionerd to function.
263278
func AsProvisionerd(ctx context.Context) context.Context {
264-
return context.WithValue(ctx, authContextKey{}, subjectProvisionerd)
279+
return context.WithValue(ctx, authContextKey{}, subjectProvisionerd(uuid.Nil))
280+
}
281+
282+
// AsProvisionerd returns a context with an actor that has permissions required
283+
// for an org scoped provisionerd to function.
284+
func AsOrganizationProvisionerd(ctx context.Context, orgID uuid.UUID) context.Context {
285+
return context.WithValue(ctx, authContextKey{}, subjectProvisionerd(orgID))
265286
}
266287

267288
// AsAutostart returns a context with an actor that has permissions required

coderd/httpmw/csrf.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler {
9393
return true
9494
}
9595

96+
if r.Header.Get(codersdk.ProvisionerDaemonKey) != "" {
97+
// If present, the provisioner daemon also is providing an api key
98+
// that will make them exempt from CSRF. But this is still useful
99+
// for enumerating the external auths.
100+
return true
101+
}
102+
96103
// If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid.
97104
// This is the CSRF check.
98105
sent := r.Header.Get("X-CSRF-TOKEN")

coderd/httpmw/provisionerdaemon.go

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/coder/coder/v2/coderd/database"
99
"github.com/coder/coder/v2/coderd/database/dbauthz"
1010
"github.com/coder/coder/v2/coderd/httpapi"
11+
"github.com/coder/coder/v2/coderd/provisionerkey"
1112
"github.com/coder/coder/v2/codersdk"
1213
)
1314

@@ -19,11 +20,13 @@ func ProvisionerDaemonAuthenticated(r *http.Request) bool {
1920
}
2021

2122
type ExtractProvisionerAuthConfig struct {
22-
DB database.Store
23-
Optional bool
23+
DB database.Store
24+
Optional bool
25+
PSK string
26+
MultiOrgEnabled bool
2427
}
2528

26-
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, psk string) func(next http.Handler) http.Handler {
29+
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig) func(next http.Handler) http.Handler {
2730
return func(next http.Handler) http.Handler {
2831
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2932
ctx := r.Context()
@@ -36,26 +39,50 @@ func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, ps
3639
httpapi.Write(ctx, w, code, response)
3740
}
3841

39-
if psk == "" {
40-
// No psk means external provisioner daemons are not allowed.
41-
// So their auth is not valid.
42-
handleOptional(http.StatusBadRequest, codersdk.Response{
43-
Message: "External provisioner daemons not enabled",
44-
})
42+
if !opts.MultiOrgEnabled {
43+
fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
4544
return
4645
}
4746

48-
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
49-
if token == "" {
47+
key := r.Header.Get(codersdk.ProvisionerDaemonKey)
48+
if key == "" {
49+
if opts.PSK == "" {
50+
handleOptional(http.StatusUnauthorized, codersdk.Response{
51+
Message: "provisioner daemon key required",
52+
})
53+
return
54+
}
55+
56+
fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
57+
return
58+
}
59+
60+
id, keyValue, err := provisionerkey.Parse(key)
61+
if err != nil {
5062
handleOptional(http.StatusUnauthorized, codersdk.Response{
51-
Message: "provisioner daemon auth token required",
63+
Message: "provisioner daemon key invalid",
64+
})
65+
return
66+
}
67+
68+
pk, err := opts.DB.GetProvisionerKeyByID(ctx, id)
69+
if err != nil {
70+
if httpapi.Is404Error(err) {
71+
handleOptional(http.StatusUnauthorized, codersdk.Response{
72+
Message: "provisioner daemon key invalid",
73+
})
74+
return
75+
}
76+
77+
handleOptional(http.StatusInternalServerError, codersdk.Response{
78+
Message: "get provisioner daemon key: " + err.Error(),
5279
})
5380
return
5481
}
5582

56-
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
83+
if subtle.ConstantTimeCompare(pk.HashedSecret, provisionerkey.HashSecret(keyValue)) != 1 {
5784
handleOptional(http.StatusUnauthorized, codersdk.Response{
58-
Message: "provisioner daemon auth token invalid",
85+
Message: "provisioner daemon key invalid",
5986
})
6087
return
6188
}
@@ -65,8 +92,33 @@ func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, ps
6592
// authenticated provisioner daemon.
6693
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
6794
// nolint:gocritic // Authenticating as a provisioner daemon.
68-
ctx = dbauthz.AsProvisionerd(ctx)
95+
ctx = dbauthz.AsOrganizationProvisionerd(ctx, pk.OrganizationID)
6996
next.ServeHTTP(w, r.WithContext(ctx))
7097
})
7198
}
7299
}
100+
101+
func fallbackToPSK(ctx context.Context, psk string, next http.Handler, w http.ResponseWriter, r *http.Request, handleOptional func(code int, response codersdk.Response)) {
102+
if psk == "" {
103+
handleOptional(http.StatusUnauthorized, codersdk.Response{
104+
Message: "External provisioner daemons not enabled",
105+
})
106+
return
107+
}
108+
109+
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
110+
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
111+
handleOptional(http.StatusUnauthorized, codersdk.Response{
112+
Message: "provisioner daemon psk invalid",
113+
})
114+
return
115+
}
116+
117+
// The PSK does not indicate a specific provisioner daemon. So just
118+
// store a boolean so the caller can check if the request is from an
119+
// authenticated provisioner daemon.
120+
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
121+
// nolint:gocritic // Authenticating as a provisioner daemon.
122+
ctx = dbauthz.AsProvisionerd(ctx)
123+
next.ServeHTTP(w, r.WithContext(ctx))
124+
}

coderd/provisionerkey/provisionerkey.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package provisionerkey
33
import (
44
"crypto/sha256"
55
"fmt"
6+
"strings"
67

78
"github.com/google/uuid"
89
"golang.org/x/xerrors"
@@ -18,14 +19,33 @@ func New(organizationID uuid.UUID, name string) (database.InsertProvisionerKeyPa
1819
if err != nil {
1920
return database.InsertProvisionerKeyParams{}, "", xerrors.Errorf("generate token: %w", err)
2021
}
21-
hashedSecret := sha256.Sum256([]byte(secret))
22+
hashedSecret := HashSecret(secret)
2223
token := fmt.Sprintf("%s:%s", id, secret)
2324

2425
return database.InsertProvisionerKeyParams{
2526
ID: id,
2627
CreatedAt: dbtime.Now(),
2728
OrganizationID: organizationID,
2829
Name: name,
29-
HashedSecret: hashedSecret[:],
30+
HashedSecret: hashedSecret,
3031
}, token, nil
3132
}
33+
34+
func Parse(token string) (uuid.UUID, string, error) {
35+
parts := strings.Split(token, ":")
36+
if len(parts) != 2 {
37+
return uuid.UUID{}, "", xerrors.Errorf("invalid token format")
38+
}
39+
40+
id, err := uuid.Parse(parts[0])
41+
if err != nil {
42+
return uuid.UUID{}, "", xerrors.Errorf("parse id: %w", err)
43+
}
44+
45+
return id, parts[1], nil
46+
}
47+
48+
func HashSecret(secret string) []byte {
49+
h := sha256.Sum256([]byte(secret))
50+
return h[:]
51+
}

codersdk/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ const (
7979
// ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
8080
ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"
8181

82+
// ProvisionerDaemonKey contains the authentication key for an external provisioner daemon
83+
ProvisionerDaemonKey = "Coder-Provisioner-Daemon-Key"
84+
8285
// BuildVersionHeader contains build information of Coder.
8386
BuildVersionHeader = "X-Coder-Build-Version"
8487

codersdk/provisionerdaemons.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ type ServeProvisionerDaemonRequest struct {
189189
Tags map[string]string `json:"tags"`
190190
// PreSharedKey is an authentication key to use on the API instead of the normal session token from the client.
191191
PreSharedKey string `json:"pre_shared_key"`
192+
// ProvisionerKey is an authentication key to use on the API instead of the normal session token from the client.
193+
ProvisionerKey string `json:"provisioner_key"`
192194
}
193195

194196
// ServeProvisionerDaemon returns the gRPC service for a provisioner daemon
@@ -223,7 +225,12 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
223225
headers := http.Header{}
224226

225227
headers.Set(BuildVersionHeader, buildinfo.Version())
226-
if req.PreSharedKey == "" {
228+
// nolint:gocritic // Need to support multiple exclusive auth flows.
229+
if req.ProvisionerKey != "" {
230+
headers.Set(ProvisionerDaemonKey, req.ProvisionerKey)
231+
} else if req.PreSharedKey != "" {
232+
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
233+
} else {
227234
// use session token if we don't have a PSK.
228235
jar, err := cookiejar.New(nil)
229236
if err != nil {
@@ -234,8 +241,6 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
234241
Value: c.SessionToken(),
235242
}})
236243
httpClient.Jar = jar
237-
} else {
238-
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
239244
}
240245

241246
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{

enterprise/coderd/coderd.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
284284
api.provisionerDaemonsEnabledMW,
285285
apiKeyMiddlewareOptional,
286286
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
287-
DB: api.Database,
288-
Optional: true,
289-
}, api.ProvisionerDaemonPSK),
287+
DB: api.Database,
288+
Optional: true,
289+
PSK: api.ProvisionerDaemonPSK,
290+
MultiOrgEnabled: api.AGPL.Experiments.Enabled(codersdk.ExperimentMultiOrganization),
291+
}),
290292
// Either a user auth or provisioner auth is required
291293
// to move forward.
292294
httpmw.RequireAPIKeyOrProvisionerDaemonAuth(),

enterprise/coderd/provisionerdaemons.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
7979

8080
type provisionerDaemonAuth struct {
8181
psk string
82+
db database.Store
8283
authorizer rbac.Authorizer
8384
}
8485

@@ -101,14 +102,21 @@ func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags
101102
}
102103
}
103104

104-
// Check for PSK
105+
// Check for provisioner key or PSK auth.
105106
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
106-
if provAuth {
107-
// If using PSK auth, the daemon is, by definition, scoped to the organization.
108-
tags = provisionersdk.MutateTags(uuid.Nil, tags)
109-
return tags, true
107+
if !provAuth {
108+
return nil, false
110109
}
111-
return nil, false
110+
111+
// ensure provisioner daemon subject can read organization
112+
_, err := p.db.GetOrganizationByID(ctx, orgID)
113+
if err != nil {
114+
return nil, false
115+
}
116+
117+
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
118+
tags = provisionersdk.MutateTags(uuid.Nil, tags)
119+
return tags, true
112120
}
113121

114122
// Serves the provisioner daemon protobuf API over a WebSocket.
@@ -209,7 +217,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
209217
)
210218

211219
authCtx := ctx
212-
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" {
220+
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" || r.Header.Get(codersdk.ProvisionerDaemonKey) != "" {
213221
//nolint:gocritic // PSK auth means no actor in request,
214222
// so use system restricted.
215223
authCtx = dbauthz.AsSystemRestricted(ctx)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy