mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: accept provisioner keys for provisioner auth (#13972)
This commit is contained in:
@@ -110,6 +110,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
provisionerDaemonAuth: &provisionerDaemonAuth{
|
||||
psk: options.ProvisionerDaemonPSK,
|
||||
authorizer: options.Authorizer,
|
||||
db: options.Database,
|
||||
},
|
||||
}
|
||||
// This must happen before coderd initialization!
|
||||
@@ -285,9 +286,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
api.provisionerDaemonsEnabledMW,
|
||||
apiKeyMiddlewareOptional,
|
||||
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
|
||||
DB: api.Database,
|
||||
Optional: true,
|
||||
}, api.ProvisionerDaemonPSK),
|
||||
DB: api.Database,
|
||||
Optional: true,
|
||||
PSK: api.ProvisionerDaemonPSK,
|
||||
MultiOrgEnabled: api.AGPL.Experiments.Enabled(codersdk.ExperimentMultiOrganization),
|
||||
}),
|
||||
// Either a user auth or provisioner auth is required
|
||||
// to move forward.
|
||||
httpmw.RequireAPIKeyOrProvisionerDaemonAuth(),
|
||||
|
||||
@@ -79,36 +79,58 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type provisionerDaemonAuth struct {
|
||||
psk string
|
||||
db database.Store
|
||||
authorizer rbac.Authorizer
|
||||
}
|
||||
|
||||
// authorize returns mutated tags and true if the given HTTP request is authorized to access the provisioner daemon
|
||||
// protobuf API, and returns nil, false otherwise.
|
||||
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string, bool) {
|
||||
// authorize returns mutated tags if the given HTTP request is authorized to access the provisioner daemon
|
||||
// protobuf API, and returns nil, err otherwise.
|
||||
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string, error) {
|
||||
ctx := r.Context()
|
||||
apiKey, ok := httpmw.APIKeyOptional(r)
|
||||
if ok {
|
||||
apiKey, apiKeyOK := httpmw.APIKeyOptional(r)
|
||||
pk, pkOK := httpmw.ProvisionerKeyAuthOptional(r)
|
||||
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
|
||||
if !provAuth && !apiKeyOK {
|
||||
return nil, xerrors.New("no API key or provisioner key provided")
|
||||
}
|
||||
if apiKeyOK && pkOK {
|
||||
return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
|
||||
}
|
||||
|
||||
if apiKeyOK {
|
||||
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
|
||||
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
|
||||
// Any authenticated user can create provisioner daemons scoped
|
||||
// for jobs that they own,
|
||||
return tags, true
|
||||
return tags, nil
|
||||
}
|
||||
ua := httpmw.UserAuthorization(r)
|
||||
if err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID)); err == nil {
|
||||
// User is allowed to create provisioner daemons
|
||||
return tags, true
|
||||
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
|
||||
if err != nil {
|
||||
if !provAuth {
|
||||
return nil, xerrors.New("user unauthorized")
|
||||
}
|
||||
|
||||
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
|
||||
// This is to preserve backwards compatibility with existing user provisioner daemons.
|
||||
// If using PSK auth, the daemon is, by definition, scoped to the organization.
|
||||
tags = provisionersdk.MutateTags(uuid.Nil, tags)
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// User is allowed to create provisioner daemons
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
if pkOK {
|
||||
if pk.OrganizationID != orgID {
|
||||
return nil, xerrors.New("provisioner key unauthorized")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for PSK
|
||||
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
|
||||
if provAuth {
|
||||
// If using PSK auth, the daemon is, by definition, scoped to the organization.
|
||||
tags = provisionersdk.MutateTags(uuid.Nil, tags)
|
||||
return tags, true
|
||||
}
|
||||
return nil, false
|
||||
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
|
||||
tags = provisionersdk.MutateTags(uuid.Nil, tags)
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Serves the provisioner daemon protobuf API over a WebSocket.
|
||||
@@ -171,12 +193,13 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
|
||||
api.Logger.Warn(ctx, "unnamed provisioner daemon")
|
||||
}
|
||||
|
||||
tags, authorized := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
|
||||
if !authorized {
|
||||
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags))
|
||||
tags, err := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags), slog.Error(err))
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden,
|
||||
codersdk.Response{
|
||||
Message: fmt.Sprintf("You aren't allowed to create provisioner daemons with scope %q", tags[provisionersdk.TagScope]),
|
||||
Detail: err.Error(),
|
||||
},
|
||||
)
|
||||
return
|
||||
@@ -209,7 +232,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
|
||||
)
|
||||
|
||||
authCtx := ctx
|
||||
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" {
|
||||
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" || r.Header.Get(codersdk.ProvisionerDaemonKey) != "" {
|
||||
//nolint:gocritic // PSK auth means no actor in request,
|
||||
// so use system restricted.
|
||||
authCtx = dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/provisionerkey"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -552,6 +555,174 @@ func TestProvisionerDaemonServe(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, daemons, 0)
|
||||
})
|
||||
|
||||
t.Run("ProvisionerKeyAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
insertParams, token, err := provisionerkey.New(uuid.Nil, "dont-TEST-me")
|
||||
require.NoError(t, err)
|
||||
|
||||
tcs := []struct {
|
||||
name string
|
||||
psk string
|
||||
multiOrgFeatureEnabled bool
|
||||
multiOrgExperimentEnabled bool
|
||||
insertParams database.InsertProvisionerKeyParams
|
||||
requestProvisionerKey string
|
||||
requestPSK string
|
||||
errStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "MultiOrgDisabledPSKAuthOK",
|
||||
psk: "provisionersftw",
|
||||
requestPSK: "provisionersftw",
|
||||
},
|
||||
{
|
||||
name: "MultiOrgExperimentDisabledPSKAuthOK",
|
||||
multiOrgFeatureEnabled: true,
|
||||
psk: "provisionersftw",
|
||||
requestPSK: "provisionersftw",
|
||||
},
|
||||
{
|
||||
name: "MultiOrgFeatureDisabledPSKAuthOK",
|
||||
multiOrgExperimentEnabled: true,
|
||||
psk: "provisionersftw",
|
||||
requestPSK: "provisionersftw",
|
||||
},
|
||||
{
|
||||
name: "MultiOrgEnabledPSKAuthOK",
|
||||
psk: "provisionersftw",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
requestPSK: "provisionersftw",
|
||||
},
|
||||
{
|
||||
name: "MultiOrgEnabledKeyAuthOK",
|
||||
psk: "provisionersftw",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
insertParams: insertParams,
|
||||
requestProvisionerKey: token,
|
||||
},
|
||||
{
|
||||
name: "MultiOrgEnabledPSKAuthDisabled",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
requestPSK: "provisionersftw",
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "WrongKey",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
insertParams: insertParams,
|
||||
requestProvisionerKey: "provisionersftw",
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "IdOKKeyValueWrong",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
insertParams: insertParams,
|
||||
requestProvisionerKey: insertParams.ID.String() + ":" + "wrong",
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "IdWrongKeyValueOK",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
insertParams: insertParams,
|
||||
requestProvisionerKey: uuid.NewString() + ":" + token,
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "KeyValueOnly",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
insertParams: insertParams,
|
||||
requestProvisionerKey: strings.Split(token, ":")[1],
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "KeyAndPSK",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
psk: "provisionersftw",
|
||||
insertParams: insertParams,
|
||||
requestProvisionerKey: token,
|
||||
requestPSK: "provisionersftw",
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "None",
|
||||
multiOrgFeatureEnabled: true,
|
||||
multiOrgExperimentEnabled: true,
|
||||
psk: "provisionersftw",
|
||||
insertParams: insertParams,
|
||||
errStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
features := license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
}
|
||||
if tc.multiOrgFeatureEnabled {
|
||||
features[codersdk.FeatureMultipleOrganizations] = 1
|
||||
}
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
if tc.multiOrgExperimentEnabled {
|
||||
dv.Experiments.Append(string(codersdk.ExperimentMultiOrganization))
|
||||
}
|
||||
client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: features,
|
||||
},
|
||||
ProvisionerDaemonPSK: tc.psk,
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
if tc.insertParams.Name != "" {
|
||||
tc.insertParams.OrganizationID = user.OrganizationID
|
||||
// nolint:gocritic // test
|
||||
_, err := db.InsertProvisionerKey(dbauthz.AsSystemRestricted(ctx), tc.insertParams)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
another := codersdk.New(client.URL)
|
||||
srv, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
|
||||
ID: uuid.New(),
|
||||
Name: testutil.MustRandString(t, 63),
|
||||
Organization: user.OrganizationID,
|
||||
Provisioners: []codersdk.ProvisionerType{
|
||||
codersdk.ProvisionerTypeEcho,
|
||||
},
|
||||
Tags: map[string]string{
|
||||
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
|
||||
},
|
||||
PreSharedKey: tc.requestPSK,
|
||||
ProvisionerKey: tc.requestProvisionerKey,
|
||||
})
|
||||
if tc.errStatusCode != 0 {
|
||||
require.Error(t, err)
|
||||
var apiError *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiError)
|
||||
require.Equal(t, http.StatusUnauthorized, apiError.StatusCode())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
err = srv.DRPCConn().Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetProvisionerDaemons(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user