feat: accept provisioner keys for provisioner auth (#13972)

This commit is contained in:
Garrett Delfosse
2024-07-25 10:22:55 -04:00
committed by GitHub
parent d488853393
commit ca83017dc1
9 changed files with 352 additions and 43 deletions
+6 -3
View File
@@ -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(),
+44 -21
View File
@@ -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) {