Add controller-led worker auth flow (#2413)

This adds an action to the API that allows pulling out an activation
token that can be given to a worker in its config (directly or via env
or file). The worker can pass this along with its normal request in
place of its normal nonce and the server will automatically accept it.

Co-authored-by: Irena Rindos <irenarindos@users.noreply.github.com>
This commit is contained in:
Jeff MitchellandIrena Rindos authored and GitHub committed 2022-09-02 14:32:41 -04:00
1 parent a1be125f5b
commit 01fb949d0b
55 files changed
+2728 -625

No files matched your search

+30 -22
View File
@@ -8,7 +8,7 @@ import (
)
// getOpts - iterate the inbound Options and return a struct
func getOpts(opt ...Option) options {
func GetOpts(opt ...Option) options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
@@ -21,26 +21,27 @@ type Option func(*options)
// options = how options are represented
type options struct {
withName string
withPublicId string
withDescription string
withAddress string
withLimit int
withLiveness time.Duration
withUpdateTags bool
withWorkerTags []*Tag
withWorkerKeyIdentifier string
withWorkerKeys WorkerKeys
withControllerEncryptionPrivateKey []byte
withKeyId string
withNonce []byte
withNewIdFunc func(context.Context) (string, error)
withFetchNodeCredentialsRequest *types.FetchNodeCredentialsRequest
withTestPkiWorkerAuthorized bool
withTestPkiWorkerKeyId *string
withWorkerType WorkerType
withRoot string
withStopAfter uint
withName string
withPublicId string
withDescription string
withAddress string
withLimit int
withLiveness time.Duration
withUpdateTags bool
withWorkerTags []*Tag
withWorkerKeyIdentifier string
withWorkerKeys WorkerKeys
withControllerEncryptionPrivateKey []byte
withKeyId string
withNonce []byte
withNewIdFunc func(context.Context) (string, error)
WithFetchNodeCredentialsRequest *types.FetchNodeCredentialsRequest
withTestPkiWorkerAuthorized bool
withTestPkiWorkerKeyId *string
withWorkerType WorkerType
withRoot string
withStopAfter uint
WithCreateControllerLedActivationToken bool
}
func getDefaultOptions() options {
@@ -153,7 +154,7 @@ func WithNewIdFunc(fn func(context.Context) (string, error)) Option {
// FetchNodeCredentialsRequest to be specified.
func WithFetchNodeCredentialsRequest(req *types.FetchNodeCredentialsRequest) Option {
return func(o *options) {
o.withFetchNodeCredentialsRequest = req
o.WithFetchNodeCredentialsRequest = req
}
}
@@ -188,3 +189,10 @@ func WithStopAfter(stopAfter uint) Option {
o.withStopAfter = stopAfter
}
}
// WithCreateControllerLedActivationToken provides an optional stop after count
func WithCreateControllerLedActivationToken(with bool) Option {
return func(o *options) {
o.WithCreateControllerLedActivationToken = with
}
}
+23 -17
View File
@@ -14,7 +14,7 @@ import (
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithName", func(t *testing.T) {
opts := getOpts(WithName("test"))
opts := GetOpts(WithName("test"))
testOpts := getDefaultOptions()
testOpts.withName = "test"
opts.withNewIdFunc = nil
@@ -22,7 +22,7 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithPublicId", func(t *testing.T) {
opts := getOpts(WithPublicId("test"))
opts := GetOpts(WithPublicId("test"))
testOpts := getDefaultOptions()
testOpts.withPublicId = "test"
opts.withNewIdFunc = nil
@@ -30,7 +30,7 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithDescription", func(t *testing.T) {
opts := getOpts(WithDescription("test desc"))
opts := GetOpts(WithDescription("test desc"))
testOpts := getDefaultOptions()
testOpts.withDescription = "test desc"
opts.withNewIdFunc = nil
@@ -38,7 +38,7 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithLimit", func(t *testing.T) {
opts := getOpts(WithLimit(5))
opts := GetOpts(WithLimit(5))
testOpts := getDefaultOptions()
testOpts.withLimit = 5
opts.withNewIdFunc = nil
@@ -46,7 +46,7 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithAddress", func(t *testing.T) {
opts := getOpts(WithAddress("test"))
opts := GetOpts(WithAddress("test"))
testOpts := getDefaultOptions()
testOpts.withAddress = "test"
opts.withNewIdFunc = nil
@@ -54,7 +54,7 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithLiveness", func(t *testing.T) {
opts := getOpts(WithLiveness(time.Hour))
opts := GetOpts(WithLiveness(time.Hour))
testOpts := getDefaultOptions()
testOpts.withLiveness = time.Hour
opts.withNewIdFunc = nil
@@ -62,7 +62,7 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(t, opts, testOpts)
})
t.Run("WithUpdateTags", func(t *testing.T) {
opts := getOpts(WithUpdateTags(true))
opts := GetOpts(WithUpdateTags(true))
testOpts := getDefaultOptions()
testOpts.withUpdateTags = true
opts.withNewIdFunc = nil
@@ -74,7 +74,7 @@ func Test_GetOpts(t *testing.T) {
{Key: "key1", Value: "val1"},
{Key: "key2", Value: "val2"},
}
opts := getOpts(WithWorkerTags(tags...))
opts := GetOpts(WithWorkerTags(tags...))
testOpts := getDefaultOptions()
testOpts.withWorkerTags = tags
opts.withNewIdFunc = nil
@@ -90,7 +90,7 @@ func Test_GetOpts(t *testing.T) {
workerSigningPubKey: populateBytes(20),
}
opts := getOpts(WithWorkerKeys(keys))
opts := GetOpts(WithWorkerKeys(keys))
testOpts = getDefaultOptions()
testOpts.withWorkerKeys = keys
opts.withNewIdFunc = nil
@@ -103,7 +103,7 @@ func Test_GetOpts(t *testing.T) {
key := populateBytes(20)
opts := getOpts(WithControllerEncryptionPrivateKey(key))
opts := GetOpts(WithControllerEncryptionPrivateKey(key))
testOpts = getDefaultOptions()
testOpts.withControllerEncryptionPrivateKey = key
opts.withNewIdFunc = nil
@@ -114,7 +114,7 @@ func Test_GetOpts(t *testing.T) {
assert := assert.New(t)
testOpts := getDefaultOptions()
opts := getOpts(WithKeyId("hi i'm another key id"))
opts := GetOpts(WithKeyId("hi i'm another key id"))
testOpts = getDefaultOptions()
testOpts.withKeyId = "hi i'm another key id"
opts.withNewIdFunc = nil
@@ -127,7 +127,7 @@ func Test_GetOpts(t *testing.T) {
nonce := populateBytes(20)
opts := getOpts(WithNonce(nonce))
opts := GetOpts(WithNonce(nonce))
testOpts = getDefaultOptions()
testOpts.withNonce = nonce
opts.withNewIdFunc = nil
@@ -137,7 +137,7 @@ func Test_GetOpts(t *testing.T) {
t.Run("WithNewIdFunc", func(t *testing.T) {
assert := assert.New(t)
testFn := func(context.Context) (string, error) { return "", nil }
opts := getOpts(WithNewIdFunc(testFn))
opts := GetOpts(WithNewIdFunc(testFn))
testOpts := getDefaultOptions()
testOpts.withNewIdFunc = testFn
assert.Equal(
@@ -150,7 +150,7 @@ func Test_GetOpts(t *testing.T) {
testOpts := getDefaultOptions()
var keyId string
opts := getOpts(WithTestPkiWorkerAuthorizedKeyId(&keyId))
opts := GetOpts(WithTestPkiWorkerAuthorizedKeyId(&keyId))
testOpts = getDefaultOptions()
testOpts.withTestPkiWorkerAuthorized = true
testOpts.withTestPkiWorkerKeyId = &keyId
@@ -161,19 +161,25 @@ func Test_GetOpts(t *testing.T) {
t.Run("WithWorkerType", func(t *testing.T) {
opts := getDefaultOptions()
assert.Empty(t, opts.withWorkerType)
opts = getOpts(WithWorkerType(KmsWorkerType))
opts = GetOpts(WithWorkerType(KmsWorkerType))
assert.Equal(t, KmsWorkerType, opts.withWorkerType)
})
t.Run("WithRoot", func(t *testing.T) {
opts := getDefaultOptions()
assert.Empty(t, opts.withRoot)
opts = getOpts(WithRoot("a"))
opts = GetOpts(WithRoot("a"))
assert.Equal(t, "a", opts.withRoot)
})
t.Run("WithStopAfter", func(t *testing.T) {
opts := getDefaultOptions()
assert.Empty(t, opts.withStopAfter)
opts = getOpts(WithStopAfter(10))
opts = GetOpts(WithStopAfter(10))
assert.Equal(t, uint(10), opts.withStopAfter)
})
t.Run("WithCreateControllerLedActivationToken", func(t *testing.T) {
opts := getDefaultOptions()
assert.False(t, opts.WithCreateControllerLedActivationToken)
opts = GetOpts(WithCreateControllerLedActivationToken(true))
assert.True(t, opts.WithCreateControllerLedActivationToken)
})
}
+1 -1
View File
@@ -44,7 +44,7 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "nil kms")
}
opts := getOpts(opt...)
opts := GetOpts(opt...)
if opts.withLimit == 0 {
// zero signals the boundary defaults should be used.
opts.withLimit = db.DefaultLimit
+1 -1
View File
@@ -17,7 +17,7 @@ func (r *Repository) ListControllers(ctx context.Context, opt ...Option) ([]*sto
// WithLimit option or the repo defaultLimit. It accepts a reader, allowing it
// to be used within a transaction or without.
func (r *Repository) listControllersWithReader(ctx context.Context, reader db.Reader, opt ...Option) ([]*store.Controller, error) {
opts := getOpts(opt...)
opts := GetOpts(opt...)
liveness := opts.withLiveness
if liveness == 0 {
liveness = DefaultLiveness
+51 -19
View File
@@ -11,10 +11,12 @@ import (
"github.com/hashicorp/boundary/internal/server/store"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-dbw"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/nodeenrollment"
"github.com/hashicorp/nodeenrollment/registration"
"github.com/hashicorp/nodeenrollment/types"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
// DeleteWorker will delete a worker from the repository.
@@ -170,7 +172,7 @@ func (r *Repository) ListWorkers(ctx context.Context, scopeIds []string, opt ...
return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope ids set")
}
opts := getOpts(opt...)
opts := GetOpts(opt...)
liveness := opts.withLiveness
if liveness == 0 {
liveness = DefaultLiveness
@@ -233,7 +235,7 @@ func (r *Repository) ListWorkers(ctx context.Context, scopeIds []string, opt ...
func (r *Repository) UpsertWorkerStatus(ctx context.Context, worker *Worker, opt ...Option) (*Worker, error) {
const op = "server.UpsertWorkerStatus"
opts := getOpts(opt...)
opts := GetOpts(opt...)
switch {
case worker == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "worker is nil")
@@ -492,10 +494,15 @@ func (r *Repository) UpdateWorker(ctx context.Context, worker *Worker, version u
// ReportedStatus and Tags are intentionally ignored when creating a worker (not
// included). Currently, a worker can only be created in the global scope
//
// Options supported: WithFetchNodeCredentialsRequest and WithNewIdFunc (this
// option is likely only useful for tests)
// Options supported: WithNewIdFunc (this option is likely only useful for
// tests), WithFetchNodeCredentialsRequest,
// WithCreateControllerLedActivationToken. The latter two are mutually
// exclusive.
func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Option) (*Worker, error) {
const op = "server.CreateWorker"
opts := GetOpts(opt...)
switch {
case worker == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing worker")
@@ -509,9 +516,10 @@ func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Op
return nil, errors.New(ctx, errors.InvalidParameter, op, "address is not empty")
case worker.LastStatusTime != nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "last status time is not nil")
case opts.WithFetchNodeCredentialsRequest != nil && opts.WithCreateControllerLedActivationToken:
return nil, errors.New(ctx, errors.InvalidParameter, op, "fetch node credentials request and controller led activation token option cannot both be set")
}
opts := getOpts(opt...)
var err error
if worker.PublicId, err = opts.withNewIdFunc(ctx); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate worker id"))
@@ -523,14 +531,11 @@ func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Op
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating state struct", err))
}
var databaseWrapper wrapping.Wrapper
var workerAuthRepo *WorkerAuthRepositoryStorage
if opts.withFetchNodeCredentialsRequest != nil {
// used to encrypt the privKey within the NodeInformation
databaseWrapper, err = r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
databaseWrapper, err := r.kms.GetWrapper(context.Background(), worker.ScopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
var returnedWorker *Worker
@@ -547,12 +552,14 @@ func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Op
); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker"))
}
if opts.withFetchNodeCredentialsRequest != nil {
switch {
case opts.WithFetchNodeCredentialsRequest != nil:
workerAuthRepo, err = NewRepositoryStorage(ctx, read, w, r.kms)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker auth repository"))
}
nodeInfo, err := registration.AuthorizeNode(ctx, workerAuthRepo, opts.withFetchNodeCredentialsRequest, nodeenrollment.WithSkipStorage(true))
nodeInfo, err := registration.AuthorizeNode(ctx, workerAuthRepo, opts.WithFetchNodeCredentialsRequest, nodeenrollment.WithSkipStorage(true))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to authorize node"))
}
@@ -560,7 +567,35 @@ func (r *Repository) CreateWorker(ctx context.Context, worker *Worker, opt ...Op
if err := StoreNodeInformationTx(ctx, w, databaseWrapper, nodeInfo); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to store node information"))
}
case opts.WithCreateControllerLedActivationToken:
tokenId, activationToken, err := registration.CreateServerLedActivationToken(ctx, nil, &types.ServerLedRegistrationRequest{},
nodeenrollment.WithSkipStorage(true),
nodeenrollment.WithState(state))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create controller-led activation token"))
}
creationTime := timestamppb.Now()
creationTimeBytes, err := proto.Marshal(creationTime)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal timestamp for activation token"))
}
activationTokenEntry, err := newWorkerAuthServerLedActivationToken(ctx, worker.PublicId, tokenId, creationTimeBytes)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in-memory activation token"))
}
if err := activationTokenEntry.encrypt(ctx, databaseWrapper); err != nil {
return errors.Wrap(ctx, err, op)
}
if err := w.Create(
ctx,
activationTokenEntry,
); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create worker activation token in storage"))
}
returnedWorker.ControllerGeneratedActivationToken = activationToken
}
return nil
},
); err != nil {
@@ -590,10 +625,7 @@ func (r *Repository) AddWorkerTags(ctx context.Context, workerId string, workerV
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("no worker found with public id %s", workerId))
}
newTags := worker.apiTags
for _, t := range tags {
newTags = append(newTags, t)
}
newTags := append(worker.apiTags, tags...)
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
worker := worker.clone()
worker.PublicId = workerId
+57 -32
View File
@@ -2,6 +2,7 @@ package server_test
import (
"context"
"crypto/rand"
"fmt"
"strings"
"testing"
@@ -143,6 +144,7 @@ func TestLookupWorkerIdByKeyId(t *testing.T) {
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
repo, err := server.NewRepository(rw, rw, kmsCache)
require.NoError(t, err)
ctx := context.Background()
@@ -252,6 +254,7 @@ func TestUpsertWorkerStatus(t *testing.T) {
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
repo, err := server.NewRepository(rw, rw, kmsCache)
require.NoError(t, err)
@@ -496,9 +499,11 @@ func TestListWorkers(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kms := kms.TestKms(t, conn, wrapper)
kmsCache := kms.TestKms(t, conn, wrapper)
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
const testLimit = 10
repo, err := server.NewRepository(rw, rw, kms, server.WithLimit(testLimit))
repo, err := server.NewRepository(rw, rw, kmsCache, server.WithLimit(testLimit))
require.NoError(t, err)
ctx := context.Background()
@@ -696,7 +701,6 @@ func TestRepository_CreateWorker(t *testing.T) {
name string
setup func() *server.Worker
repo *server.Repository
fetchReq *types.FetchNodeCredentialsRequest
reader db.Reader
opt []server.Option
wantErr bool
@@ -812,19 +816,21 @@ func TestRepository_CreateWorker(t *testing.T) {
return w
},
reader: rw,
fetchReq: func() *types.FetchNodeCredentialsRequest {
// This happens on the worker
fileStorage, err := file.New(testCtx)
require.NoError(t, err)
defer fileStorage.Cleanup()
opt: []server.Option{server.WithFetchNodeCredentialsRequest(
func() *types.FetchNodeCredentialsRequest {
// This happens on the worker
fileStorage, err := file.New(testCtx)
require.NoError(t, err)
defer fileStorage.Cleanup()
nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage)
require.NoError(t, err)
// Create request using worker id
fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx)
require.NoError(t, err)
return fetchReq
}(),
nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage)
require.NoError(t, err)
// Create request using worker id
fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx)
require.NoError(t, err)
return fetchReq
}(),
)},
repo: func() *server.Repository {
mockConn, mock := db.TestSetupWithMock(t)
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"version", "create_time"}).AddRow(migrations.Version, time.Now()))
@@ -844,7 +850,7 @@ func TestRepository_CreateWorker(t *testing.T) {
return w
},
reader: rw,
fetchReq: &types.FetchNodeCredentialsRequest{},
opt: []server.Option{server.WithFetchNodeCredentialsRequest(&types.FetchNodeCredentialsRequest{})},
repo: testRepo,
wantErr: true,
wantErrContains: "unable to authorize node",
@@ -862,7 +868,7 @@ func TestRepository_CreateWorker(t *testing.T) {
wantErrIs: errors.NotUnique,
},
{
name: "success",
name: "success-no-options",
setup: func() *server.Worker {
w := server.NewWorker(scope.Global.String())
w.Name = "success"
@@ -892,29 +898,40 @@ func TestRepository_CreateWorker(t *testing.T) {
return w
},
reader: rw,
fetchReq: func() *types.FetchNodeCredentialsRequest {
// This happens on the worker
fileStorage, err := file.New(testCtx)
require.NoError(t, err)
defer fileStorage.Cleanup()
opt: []server.Option{server.WithFetchNodeCredentialsRequest(
func() *types.FetchNodeCredentialsRequest {
// This happens on the worker
fileStorage, err := file.New(testCtx)
require.NoError(t, err)
defer fileStorage.Cleanup()
nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage)
require.NoError(t, err)
// Create request using worker id
fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx)
require.NoError(t, err)
return fetchReq
}(),
nodeCreds, err := types.NewNodeCredentials(testCtx, fileStorage)
require.NoError(t, err)
// Create request using worker id
fetchReq, err := nodeCreds.CreateFetchNodeCredentialsRequest(testCtx)
require.NoError(t, err)
return fetchReq
}(),
)},
repo: testRepo,
},
{
name: "success-with-controller-activation-token",
setup: func() *server.Worker {
w := server.NewWorker(scope.Global.String())
w.Name = "success-with-controller-activation-token"
return w
},
reader: rw,
opt: []server.Option{server.WithCreateControllerLedActivationToken(true)},
repo: testRepo,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
testWorker := tc.setup()
tc.opt = append(tc.opt, server.WithFetchNodeCredentialsRequest(tc.fetchReq))
got, err := tc.repo.CreateWorker(testCtx, testWorker, tc.opt...)
if tc.wantErr {
require.Error(err)
@@ -942,12 +959,19 @@ func TestRepository_CreateWorker(t *testing.T) {
require.NoError(err)
assert.Empty(cmp.Diff(got, found, protocmp.Transform()))
if tc.fetchReq != nil {
opts := server.GetOpts(tc.opt...)
if opts.WithFetchNodeCredentialsRequest != nil {
worker := &server.WorkerAuth{
WorkerAuth: &store.WorkerAuth{},
}
require.NoError(tc.reader.LookupWhere(testCtx, worker, "worker_id = ?", []any{found.PublicId}))
}
if opts.WithCreateControllerLedActivationToken {
activationToken := &server.WorkerAuthServerLedActivationToken{
WorkerAuthServerLedActivationToken: &store.WorkerAuthServerLedActivationToken{},
}
require.NoError(tc.reader.LookupWhere(testCtx, activationToken, "worker_id = ?", []any{found.PublicId}))
}
})
}
}
@@ -959,6 +983,7 @@ func TestRepository_UpdateWorker(t *testing.T) {
rw := db.New(conn)
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
repo, err := server.NewRepository(rw, rw, kmsCache)
require.NoError(t, err)
+104 -6
View File
@@ -9,6 +9,7 @@ import (
"github.com/fatih/structs"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
@@ -85,6 +86,7 @@ func (r *WorkerAuthRepositoryStorage) Store(ctx context.Context, msg nodee.Messa
}); err != nil {
return errors.Wrap(ctx, err, op)
}
case *types.RootCertificates:
err := r.storeRootCertificates(ctx, t)
if err != nil {
@@ -343,6 +345,9 @@ func (r *WorkerAuthRepositoryStorage) Load(ctx context.Context, msg nodee.Messag
case *types.NodeInformation:
err = r.loadNodeInformation(ctx, t)
case *types.ServerLedActivationToken:
err = r.loadServerLedActivationToken(ctx, t)
case *types.RootCertificates:
err = r.loadRootCertificates(ctx, t)
@@ -416,6 +421,48 @@ func (r *WorkerAuthRepositoryStorage) loadNodeInformation(ctx context.Context, n
return nil
}
// Node information is loaded in two parts:
// * the workerAuth record
// * its certificate bundles
func (r *WorkerAuthRepositoryStorage) loadServerLedActivationToken(ctx context.Context, token *types.ServerLedActivationToken) error {
const op = "server.(WorkerAuthRepositoryStorage).loadServerLedActivationToken"
if token == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing ServerLedActivationToken")
}
activationTokenEntry := allocWorkerAuthServerLedActivationToken()
err := r.reader.LookupWhere(ctx, activationTokenEntry, "token_id = ?", []any{token.Id})
if err != nil {
if errors.Is(err, dbw.ErrRecordNotFound) {
return nodee.ErrNotFound
}
return errors.Wrap(ctx, err, op)
}
token.State, err = AttachWorkerIdToState(ctx, activationTokenEntry.WorkerId)
if err != nil {
return errors.Wrap(ctx, err, op)
}
// Decrypt marshaled creation time
databaseWrapper, err := r.kms.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeDatabase, kms.WithKeyId(activationTokenEntry.KeyId))
if err != nil {
return errors.Wrap(ctx, err, op)
}
if err := activationTokenEntry.decrypt(ctx, databaseWrapper); err != nil {
return errors.Wrap(ctx, err, op)
}
token.CreationTimeMarshaled = activationTokenEntry.CreationTime
token.CreationTime = new(timestamppb.Timestamp)
if err := proto.Unmarshal(token.CreationTimeMarshaled, token.CreationTime); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
func (r *WorkerAuthRepositoryStorage) findCertBundles(ctx context.Context, workerKeyId string) ([]*types.CertificateBundle, error) {
const op = "server.(WorkerAuthRepositoryStorage).findCertBundles"
if workerKeyId == "" {
@@ -537,9 +584,14 @@ func (r *WorkerAuthRepositoryStorage) Remove(ctx context.Context, msg nodee.Mess
}
// Determine type of message to remove
switch msg.(type) {
switch t := msg.(type) {
case *types.NodeInformation:
err := r.removeNodeInformation(ctx, msg.GetId())
err := r.removeNodeInformation(ctx, t)
if err != nil {
return errors.Wrap(ctx, err, op)
}
case *types.ServerLedActivationToken:
err := r.removeServerLedActivationToken(ctx, t)
if err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -555,9 +607,12 @@ func (r *WorkerAuthRepositoryStorage) Remove(ctx context.Context, msg nodee.Mess
return nil
}
func (r *WorkerAuthRepositoryStorage) removeNodeInformation(ctx context.Context, id string) error {
func (r *WorkerAuthRepositoryStorage) removeNodeInformation(ctx context.Context, msg *types.NodeInformation) error {
const op = "server.(WorkerAuthRepositoryStorage).removeNodeInformation"
if id == "" {
switch {
case msg == nil:
return errors.New(ctx, errors.InvalidParameter, op, "nil node information")
case msg.Id == "":
return errors.New(ctx, errors.InvalidParameter, op, "empty id")
}
@@ -567,11 +622,11 @@ func (r *WorkerAuthRepositoryStorage) removeNodeInformation(ctx context.Context,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
var err error
_, err = w.Exec(ctx, deleteWorkerAuthQuery, []interface{}{sql.Named("worker_key_identifier", id)})
_, err = w.Exec(ctx, deleteWorkerAuthQuery, []interface{}{sql.Named("worker_key_identifier", msg.Id)})
if err != nil {
return errors.Wrap(ctx, err, op)
}
_, err = w.Exec(ctx, deleteWorkerCertBundlesQuery, []interface{}{sql.Named("worker_key_identifier", id)})
_, err = w.Exec(ctx, deleteWorkerCertBundlesQuery, []interface{}{sql.Named("worker_key_identifier", msg.Id)})
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete workerAuth"))
}
@@ -585,6 +640,49 @@ func (r *WorkerAuthRepositoryStorage) removeNodeInformation(ctx context.Context,
return nil
}
func (r *WorkerAuthRepositoryStorage) removeServerLedActivationToken(ctx context.Context, msg *types.ServerLedActivationToken) error {
const op = "server.(WorkerAuthRepositoryStorage).removeServerLedActivationToken"
switch {
case msg == nil:
return errors.New(ctx, errors.InvalidParameter, op, "nil node information")
case msg.Id == "":
return errors.New(ctx, errors.InvalidParameter, op, "empty id")
case msg.State == nil:
return errors.New(ctx, errors.InvalidParameter, op, "missing state")
}
var workerInfo workerAuthWorkerId
if err := mapstructure.Decode(msg.State.AsMap(), &workerInfo); err != nil {
return errors.Wrap(ctx, err, op)
}
if workerInfo.WorkerId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "state missing worker id")
}
actToken := allocWorkerAuthServerLedActivationToken()
actToken.WorkerId = workerInfo.WorkerId
_, err := r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
rowsDeleted, err := w.Delete(ctx, actToken)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if rowsDeleted != 1 {
return errors.New(ctx, errors.UnexpectedRowsAffected, op, fmt.Sprintf("expected to delete one activation token, deleted %d", rowsDeleted))
}
return nil
},
)
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
func (r *WorkerAuthRepositoryStorage) removeCertificateAuthority(ctx context.Context, cert *types.RootCertificates) error {
const op = "server.(WorkerAuthRepositoryStorage).removeCertificateAuthority"
+55 -10
View File
@@ -2,13 +2,18 @@ package server
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"strings"
"testing"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/server/store"
"github.com/hashicorp/boundary/internal/types/scope"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/nodeenrollment"
@@ -17,9 +22,11 @@ import (
"github.com/hashicorp/nodeenrollment/storage/file"
"github.com/hashicorp/nodeenrollment/types"
"github.com/mitchellh/mapstructure"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/curve25519"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -135,18 +142,14 @@ func TestStoreCertAuthorityVersioning(t *testing.T) {
func TestStoreWorkerAuth(t *testing.T) {
require, assert := require.New(t), assert.New(t)
ctx := context.Background()
wrapper := db.TestWrapper(t)
rootWrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, wrapper)
kmsCache := kms.TestKms(t, conn, rootWrapper)
// Ensures the global scope contains a valid root key
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
require.NoError(err)
wrapper, err = kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase)
require.NoError(err)
require.NotNil(wrapper)
require.NoError(kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
worker := TestPkiWorker(t, conn, wrapper)
worker := TestPkiWorker(t, conn, rootWrapper)
rw := db.New(conn)
rootStorage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
@@ -216,6 +219,49 @@ func TestStoreWorkerAuth(t *testing.T) {
require.Error(err)
}
func TestStoreServerLedActivationToken(t *testing.T) {
require, assert := require.New(t), assert.New(t)
ctx := context.Background()
rootWrapper := db.TestWrapper(t)
conn, _ := db.TestSetup(t, "postgres")
kmsCache := kms.TestKms(t, conn, rootWrapper)
// Ensure the global scope contains a valid root key
require.NoError(kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
rw := db.New(conn)
rootStorage, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
require.NoError(err)
_, err = rotation.RotateRootCertificates(ctx, rootStorage)
require.NoError(err)
repo, err := NewRepository(rw, rw, kmsCache)
require.NoError(err)
worker, err := repo.CreateWorker(ctx, &Worker{Worker: &store.Worker{ScopeId: scope.Global.String()}}, WithCreateControllerLedActivationToken(true))
require.NoError(err)
require.NotEmpty(worker.ControllerGeneratedActivationToken)
// We should now look for an activation token value in storage using the
// lookup function and validate that it's populated
tokenNonce := new(types.ServerLedActivationTokenNonce)
marshaledNonce, err := base58.FastBase58Decoding(strings.TrimPrefix(worker.ControllerGeneratedActivationToken, nodeenrollment.ServerLedActivationTokenPrefix))
require.NoError(err)
require.NoError(proto.Unmarshal(marshaledNonce, tokenNonce))
hm := hmac.New(sha256.New, tokenNonce.HmacKeyBytes)
idBytes := hm.Sum(tokenNonce.Nonce)
actToken := &types.ServerLedActivationToken{
Id: base58.FastBase58Encoding(idBytes),
}
require.NoError(rootStorage.Load(ctx, actToken))
require.NotNil(actToken.CreationTime)
assert.False(actToken.CreationTime.AsTime().IsZero())
assert.Less(time.Until(actToken.CreationTime.AsTime()), time.Duration(0))
require.NoError(rootStorage.Remove(ctx, actToken))
require.Error(rootStorage.Load(ctx, actToken))
}
func TestUnsupportedMessages(t *testing.T) {
require := require.New(t)
ctx := context.Background()
@@ -253,8 +299,7 @@ func TestStoreNodeInformationTx(t *testing.T) {
kmsCache := kms.TestKms(t, conn, testWrapper)
// Ensures the global scope contains a valid root key
err = kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
require.NoError(t, err)
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), scope.Global.String(), kms.KeyPurposeDatabase)
require.NoError(t, err)
+2 -2
View File
@@ -1,11 +1,11 @@
package store
// TableName overrides the table name used by Controller to `server_controller`
func (Controller) TableName() string {
func (*Controller) TableName() string {
return "server_controller"
}
// TableName overrides the table name used by WorkerTag to `worker_tag`
func (WorkerTag) TableName() string {
func (*WorkerTag) TableName() string {
return "server_worker_tag"
}
+128 -9
View File
@@ -203,6 +203,99 @@ func (x *WorkerCertBundle) GetCertBundle() []byte {
return nil
}
// WorkerAuthServerLedActivationToken contains all fields related to a
// WorkerAuthServerLedActivationToken resource
type WorkerAuthServerLedActivationToken struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The worker_id of the worker that this activates
// @inject_tag: `gorm:"primary_key"`
WorkerId string `protobuf:"bytes,10,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty" gorm:"primary_key"`
// The token identifier, which is used for lookup
// @inject_tag: `gorm:"not_null"`
TokenId string `protobuf:"bytes,15,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty" gorm:"not_null"`
// The creation time, encrypted to prevent tampering, as the time plus
// existence of the record allows authorization
// @inject_tag: `gorm:"not_null" wrapping:"ct,creation_time_data"`
CreationTimeEncrypted []byte `protobuf:"bytes,20,opt,name=creation_time_encrypted,json=creationTimeEncrypted,proto3" json:"creation_time_encrypted,omitempty" gorm:"not_null" wrapping:"ct,creation_time_data"`
// The plaintext bytes of the creation time, which are never stored. This is a
// marshaled timestamppb.Timestamp.
// @inject_tag: `gorm:"-" wrapping:"pt,creation_time_data"`
CreationTime []byte `protobuf:"bytes,21,opt,name=creation_time,json=creationTime,proto3" json:"creation_time,omitempty" gorm:"-" wrapping:"pt,creation_time_data"`
// The key ID of the encrypting key
// @inject_tag: `gorm:"not_null"`
KeyId string `protobuf:"bytes,40,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"`
}
func (x *WorkerAuthServerLedActivationToken) Reset() {
*x = WorkerAuthServerLedActivationToken{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *WorkerAuthServerLedActivationToken) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WorkerAuthServerLedActivationToken) ProtoMessage() {}
func (x *WorkerAuthServerLedActivationToken) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WorkerAuthServerLedActivationToken.ProtoReflect.Descriptor instead.
func (*WorkerAuthServerLedActivationToken) Descriptor() ([]byte, []int) {
return file_controller_storage_servers_store_v1_worker_auth_proto_rawDescGZIP(), []int{2}
}
func (x *WorkerAuthServerLedActivationToken) GetWorkerId() string {
if x != nil {
return x.WorkerId
}
return ""
}
func (x *WorkerAuthServerLedActivationToken) GetTokenId() string {
if x != nil {
return x.TokenId
}
return ""
}
func (x *WorkerAuthServerLedActivationToken) GetCreationTimeEncrypted() []byte {
if x != nil {
return x.CreationTimeEncrypted
}
return nil
}
func (x *WorkerAuthServerLedActivationToken) GetCreationTime() []byte {
if x != nil {
return x.CreationTime
}
return nil
}
func (x *WorkerAuthServerLedActivationToken) GetKeyId() string {
if x != nil {
return x.KeyId
}
return ""
}
var File_controller_storage_servers_store_v1_worker_auth_proto protoreflect.FileDescriptor
var file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = []byte{
@@ -242,11 +335,24 @@ var file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc = []byte{
0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e,
0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x62,
0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x65, 0x72,
0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f,
0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x22, 0x57, 0x6f, 0x72, 0x6b,
0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4c, 0x65, 0x64, 0x41,
0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b,
0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x74,
0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74,
0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x12, 0x23,
0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x15, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54,
0x69, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x28, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f,
0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72,
0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -261,10 +367,11 @@ func file_controller_storage_servers_store_v1_worker_auth_proto_rawDescGZIP() []
return file_controller_storage_servers_store_v1_worker_auth_proto_rawDescData
}
var file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_controller_storage_servers_store_v1_worker_auth_proto_goTypes = []interface{}{
(*WorkerAuth)(nil), // 0: controller.storage.servers.store.v1.WorkerAuth
(*WorkerCertBundle)(nil), // 1: controller.storage.servers.store.v1.WorkerCertBundle
(*WorkerAuth)(nil), // 0: controller.storage.servers.store.v1.WorkerAuth
(*WorkerCertBundle)(nil), // 1: controller.storage.servers.store.v1.WorkerCertBundle
(*WorkerAuthServerLedActivationToken)(nil), // 2: controller.storage.servers.store.v1.WorkerAuthServerLedActivationToken
}
var file_controller_storage_servers_store_v1_worker_auth_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
@@ -304,6 +411,18 @@ func file_controller_storage_servers_store_v1_worker_auth_proto_init() {
return nil
}
}
file_controller_storage_servers_store_v1_worker_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkerAuthServerLedActivationToken); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -311,7 +430,7 @@ func file_controller_storage_servers_store_v1_worker_auth_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_storage_servers_store_v1_worker_auth_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
+2 -2
View File
@@ -99,7 +99,7 @@ func TestKmsWorker(t *testing.T, conn *db.DB, wrapper wrapping.Wrapper, opt ...O
serversRepo, err := NewRepository(rw, rw, kms)
require.NoError(t, err)
ctx := context.Background()
opts := getOpts(opt...)
opts := GetOpts(opt...)
namePart, err := newWorkerId(ctx)
require.NoError(t, err)
@@ -150,7 +150,7 @@ func TestPkiWorker(t *testing.T, conn *db.DB, wrapper wrapping.Wrapper, opt ...O
serversRepo, err := NewRepository(rw, rw, kmsCache)
require.NoError(t, err)
ctx := context.Background()
opts := getOpts(opt...)
opts := GetOpts(opt...)
require.NoError(t, err)
wrk := NewWorker(scope.Global.String(),
+4
View File
@@ -2,10 +2,12 @@ package server
import (
"context"
"crypto/rand"
"strings"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -38,6 +40,8 @@ func TestTestKmsWorker(t *testing.T) {
func TestTestPkiWorker(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
require.NoError(t, kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
const (
name = "test name"
description = "test description"
+4 -1
View File
@@ -67,13 +67,16 @@ type Worker struct {
// other based on the context in which the worker is passed. As such
// inputTags should only be read when performing mutations on the database.
inputTags []*Tag `gorm:"-"`
// This is used to pass the token back to the calling function
ControllerGeneratedActivationToken string `gorm:"-"`
}
// NewWorker returns a new Worker. Valid options are WithName, WithDescription
// WithAddress, and WithWorkerTags. All other options are ignored. This does
// not set any of the worker reported values.
func NewWorker(scopeId string, opt ...Option) *Worker {
opts := getOpts(opt...)
opts := GetOpts(opt...)
return &Worker{
Worker: &store.Worker{
ScopeId: scopeId,
+1 -1
View File
@@ -24,7 +24,7 @@ type WorkerKeys struct {
func newWorkerAuth(ctx context.Context, workerKeyIdentifier, workerId string, opt ...Option) (*WorkerAuth, error) {
const op = "server.newWorkerAuth"
opts := getOpts(opt...)
opts := GetOpts(opt...)
if workerKeyIdentifier == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no workerKeyIdentifier")
@@ -0,0 +1,113 @@
package server
import (
"context"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/server/store"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"google.golang.org/protobuf/proto"
)
// WorkerAuthServerLedActivationToken contains an activation token for a worker
type WorkerAuthServerLedActivationToken struct {
*store.WorkerAuthServerLedActivationToken
tableName string `gorm:"-"`
}
// newWorkerAuthServerLedActivation creates a new token from the given values
func newWorkerAuthServerLedActivationToken(ctx context.Context, workerId, tokenId string, creationTime []byte, _ ...Option) (*WorkerAuthServerLedActivationToken, error) {
const op = "server.newWorkerAuthServerLedActivationToken"
switch {
case workerId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "no worker id")
case tokenId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "no token id")
case len(creationTime) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty creation time")
}
l := &WorkerAuthServerLedActivationToken{
WorkerAuthServerLedActivationToken: &store.WorkerAuthServerLedActivationToken{
WorkerId: workerId,
TokenId: tokenId,
CreationTime: creationTime,
},
}
return l, nil
}
func allocWorkerAuthServerLedActivationToken() *WorkerAuthServerLedActivationToken {
return &WorkerAuthServerLedActivationToken{
WorkerAuthServerLedActivationToken: &store.WorkerAuthServerLedActivationToken{},
}
}
func (w *WorkerAuthServerLedActivationToken) clone() *WorkerAuthServerLedActivationToken {
cp := proto.Clone(w.WorkerAuthServerLedActivationToken)
return &WorkerAuthServerLedActivationToken{
WorkerAuthServerLedActivationToken: cp.(*store.WorkerAuthServerLedActivationToken),
}
}
// ValidateNewWorkerAuthServerLedActivationToken is called before storing a WorkerAuthActivationToken in the db
func (w *WorkerAuthServerLedActivationToken) ValidateNewWorkerAuthServerLedActivationToken(ctx context.Context) error {
const op = "server.(WorkerAuthServerLedActivationToken).ValidateNewWorkerAuthServerLedActivationToken"
if w.WorkerId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing worker id")
}
if w.TokenId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing token id")
}
if len(w.CreationTimeEncrypted) == 0 {
return errors.New(ctx, errors.InvalidParameter, op, "missing encrypted creation time")
}
return nil
}
// TableName returns the table name.
func (w *WorkerAuthServerLedActivationToken) TableName() string {
if w.tableName != "" {
return w.tableName
}
return "worker_auth_server_led_activation_token"
}
// SetTableName sets the table name.
func (w *WorkerAuthServerLedActivationToken) SetTableName(n string) {
w.tableName = n
}
// encrypt the activation token before storing
func (w *WorkerAuthServerLedActivationToken) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "server.(WorkerAuthServerLedActivationToken).encrypt"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.WrapStruct(ctx, cipher, w.WorkerAuthServerLedActivationToken); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
keyId, err := cipher.KeyId(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
}
w.KeyId = keyId
return nil
}
// decrypt the auth method after reading it from the db
func (w *WorkerAuthServerLedActivationToken) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "server.(WorkerAuthServerLedActivationToken).decrypt"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
if err := structwrapping.UnwrapStruct(ctx, cipher, w.WorkerAuthServerLedActivationToken); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
return nil
}
@@ -0,0 +1,122 @@
package server
import (
"context"
"crypto/rand"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-dbw"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWorkerAuthActivationTokenConstraints(t *testing.T) {
t.Parallel()
tlRequire, tlAssert := require.New(t), assert.New(t)
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
rw := db.New(conn)
kmsCache := kms.TestKms(t, conn, wrap)
tlRequire.NoError(kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader)))
repo, err := NewRepository(rw, rw, kmsCache)
tlRequire.NoError(err)
// First create a worker without an activation token so we can verify it doesn't show up in the table
origWorker := NewWorker(scope.Global.String())
origWorker, err = repo.CreateWorker(ctx, origWorker)
tlRequire.NoError(err)
tlAssert.Empty(origWorker.ControllerGeneratedActivationToken)
activationToken := allocWorkerAuthServerLedActivationToken()
err = rw.LookupWhere(ctx, activationToken, "worker_id = ?", []any{origWorker.PublicId})
tlRequire.Error(err)
tlAssert.ErrorIs(err, dbw.ErrRecordNotFound)
// Now create a worker with an activation token and verify we can find it
worker := NewWorker(scope.Global.String())
worker, err = repo.CreateWorker(ctx, worker, WithCreateControllerLedActivationToken(true))
tlRequire.NoError(err)
tlAssert.NotEmpty(worker.ControllerGeneratedActivationToken)
activationToken = allocWorkerAuthServerLedActivationToken()
err = rw.LookupWhere(ctx, activationToken, "worker_id = ?", []any{worker.PublicId})
tlRequire.NoError(err)
tlAssert.Empty(activationToken.CreationTime)
tlAssert.NotEmpty(activationToken.KeyId)
tlAssert.NotEmpty(activationToken.TokenId)
tlAssert.NotEmpty(activationToken.CreationTimeEncrypted)
// Run some test cases. All are expected to result in error.
cases := []struct {
name string
createModifyFn func(*testing.T, *WorkerAuthServerLedActivationToken) *WorkerAuthServerLedActivationToken
updateModifyFn func(*testing.T, *WorkerAuthServerLedActivationToken) (string, []any) // If set, will update instead of create
wantErrContains string
}{
{
name: "fully-duplicate",
createModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) *WorkerAuthServerLedActivationToken {
return in
},
wantErrContains: "worker_auth_server_led_activation_token_pkey",
},
{
name: "same-token-id",
createModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) *WorkerAuthServerLedActivationToken {
in.WorkerId = origWorker.PublicId
return in
},
wantErrContains: "worker_auth_server_led_activation_token_token_id_uq",
},
{
name: "same-worker-id",
createModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) *WorkerAuthServerLedActivationToken {
in.TokenId = in.TokenId[0:5]
return in
},
wantErrContains: "worker_auth_server_led_activation_token_pkey",
},
{
name: "modify-worker-id",
updateModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) (string, []any) {
return "update worker_auth_server_led_activation_token set worker_id = ? where token_id = ?", []any{origWorker.PublicId, in.TokenId}
},
wantErrContains: "immutable column: worker_auth_server_led_activation_token.worker_id: integrity violation",
},
{
name: "modify-token-id",
updateModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) (string, []any) {
return "update worker_auth_server_led_activation_token set token_id = ? where worker_id = ?", []any{in.TokenId[0:5], in.WorkerId}
},
wantErrContains: "immutable column: worker_auth_server_led_activation_token.token_id: integrity violation",
},
{
name: "modify-create-time",
updateModifyFn: func(t *testing.T, in *WorkerAuthServerLedActivationToken) (string, []any) {
return "update worker_auth_server_led_activation_token set creation_time_encrypted = ? where worker_id = ?", []any{in.CreationTimeEncrypted[0:5], in.WorkerId}
},
wantErrContains: "immutable column: worker_auth_server_led_activation_token.creation_time_encrypted: integrity violation",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
testActivationToken := activationToken.clone()
var err error
if tc.updateModifyFn != nil {
queryStr, args := tc.updateModifyFn(t, testActivationToken)
var rowsUpdated int
rowsUpdated, err = rw.Exec(ctx, queryStr, args)
assert.Empty(rowsUpdated)
} else {
err = rw.Create(ctx, tc.createModifyFn(t, testActivationToken))
}
require.Error(err)
assert.Contains(err.Error(), tc.wantErrContains)
})
}
}