Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit596ef99

Browse files
committed
feat: accept provisioner keys for provisioner auth
1 parentb817c86 commit596ef99

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+
iforgID!=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
funcAsProvisionerd(ctx context.Context) context.Context {
264-
returncontext.WithValue(ctx,authContextKey{},subjectProvisionerd)
279+
returncontext.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+
funcAsOrganizationProvisionerd(ctx context.Context,orgID uuid.UUID) context.Context {
285+
returncontext.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
returntrue
9494
}
9595

96+
ifr.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+
returntrue
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
typeExtractProvisionerAuthConfigstruct {
22-
DB database.Store
23-
Optionalbool
23+
DB database.Store
24+
Optionalbool
25+
PSKstring
26+
MultiOrgEnabledbool
2427
}
2528

26-
funcExtractProvisionerDaemonAuthenticated(optsExtractProvisionerAuthConfig,pskstring)func(next http.Handler) http.Handler {
29+
funcExtractProvisionerDaemonAuthenticated(optsExtractProvisionerAuthConfig)func(next http.Handler) http.Handler {
2730
returnfunc(next http.Handler) http.Handler {
2831
returnhttp.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-
ifpsk=="" {
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-
iftoken=="" {
47+
key:=r.Header.Get(codersdk.ProvisionerDaemonKey)
48+
ifkey=="" {
49+
ifopts.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+
iferr!=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+
iferr!=nil {
70+
ifhttpapi.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-
ifsubtle.ConstantTimeCompare([]byte(token), []byte(psk))!=1 {
83+
ifsubtle.ConstantTimeCompare(pk.HashedSecret,provisionerkey.HashSecret(keyValue))!=1 {
5784
handleOptional(http.StatusUnauthorized, codersdk.Response{
58-
Message:"provisioner daemonauth token invalid",
85+
Message:"provisioner daemonkey 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+
funcfallbackToPSK(ctx context.Context,pskstring,next http.Handler,w http.ResponseWriter,r*http.Request,handleOptionalfunc(codeint,response codersdk.Response)) {
102+
ifpsk=="" {
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+
ifsubtle.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
iferr!=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+
funcParse(tokenstring) (uuid.UUID,string,error) {
35+
parts:=strings.Split(token,":")
36+
iflen(parts)!=2 {
37+
return uuid.UUID{},"",xerrors.Errorf("invalid token format")
38+
}
39+
40+
id,err:=uuid.Parse(parts[0])
41+
iferr!=nil {
42+
return uuid.UUID{},"",xerrors.Errorf("parse id: %w",err)
43+
}
44+
45+
returnid,parts[1],nil
46+
}
47+
48+
funcHashSecret(secretstring) []byte {
49+
h:=sha256.Sum256([]byte(secret))
50+
returnh[:]
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
Tagsmap[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
PreSharedKeystring`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+
ProvisionerKeystring`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-
ifreq.PreSharedKey=="" {
228+
// nolint:gocritic // Need to support multiple exclusive auth flows.
229+
ifreq.ProvisionerKey!="" {
230+
headers.Set(ProvisionerDaemonKey,req.ProvisionerKey)
231+
}elseifreq.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
iferr!=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
typeprovisionerDaemonAuthstruct {
8181
pskstring
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 forprovisioner key orPSK auth.
105106
provAuth:=httpmw.ProvisionerDaemonAuthenticated(r)
106-
ifprovAuth {
107-
// If using PSK auth, the daemon is, by definition, scoped to the organization.
108-
tags=provisionersdk.MutateTags(uuid.Nil,tags)
109-
returntags,true
107+
if!provAuth {
108+
returnnil,false
110109
}
111-
returnnil,false
110+
111+
// ensure provisioner daemon subject can read organization
112+
_,err:=p.db.GetOrganizationByID(ctx,orgID)
113+
iferr!=nil {
114+
returnnil,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+
returntags,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-
ifr.Header.Get(codersdk.ProvisionerDaemonPSK)!="" {
220+
ifr.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)

[8]ページ先頭

©2009-2025 Movatter.jp