Files
threshold/internal/cmd/base/dev.go
T
Timothy Messier 8caaf3e076 refactor(schema): Add support for migration editions
This refactors the schema package and subpackages to support editions.
An Edition is a collection of sql migrations. A given boundary binary
can have multiple migration editions. The `schema.Manager` is updated to
apply the necessary migrations in the correct order, leveraging an
Edition's priority to determine the order in which the separate Editions
are applied.

The sql migration files are now nested under a directory for the
edition.

This also switches from the generated code to using embed, which
eliminates the need for the `make migrations` target and the generated
code. As such this commit removes the target and generated code.

See:
    https://pkg.go.dev/embed
2021-10-08 06:50:26 -04:00

481 lines
14 KiB
Go

package base
import (
"context"
"crypto/ed25519"
"fmt"
"net"
"net/url"
"runtime"
"strings"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/cmd/base/internal/docker"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/schema"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/testing/dbtest"
capoidc "github.com/hashicorp/cap/oidc"
"github.com/hashicorp/go-multierror"
"gorm.io/gorm/logger"
)
func (b *Server) CreateDevDatabase(ctx context.Context, opt ...Option) error {
const op = "base.(Server).CreateDevDatabase"
var container, url, dialect string
var err error
var c func() error
opts := getOpts(opt...)
// We should only get back postgres for now, but laying the foundation for non-postgres
switch opts.withDialect {
case "":
event.WriteError(ctx, op, err, event.WithInfoMsg("unsupported dialect", "wanted", "postgres", "got", opts.withDialect))
default:
dialect = opts.withDialect
}
switch b.DatabaseUrl {
case "":
if opts.withDatabaseTemplate != "" {
c, url, _, err = dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(opts.withDatabaseTemplate))
} else {
c, url, container, err = docker.StartDbInDocker(dialect, docker.WithContainerImage(opts.withContainerImage))
}
// In case of an error, run the cleanup function. If we pass all errors, c should be set to a noop
// function before returning from this method
defer func() {
if !opts.withSkipDatabaseDestruction {
if c != nil {
if err := c(); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error cleaning up docker container"))
}
}
}
}()
if err == docker.ErrDockerUnsupported {
return err
}
if err != nil {
return fmt.Errorf("unable to start dev database with dialect %s: %w", dialect, err)
}
// Let migrate store manage the dirty bit since dev DBs should be ephemeral anyways.
_, err := schema.MigrateStore(ctx, schema.Dialect(dialect), url)
if err != nil {
err = fmt.Errorf("unable to initialize dev database with dialect %s: %w", dialect, err)
if c != nil {
err = multierror.Append(err, c())
}
return err
}
b.DevDatabaseCleanupFunc = c
b.DatabaseUrl = url
default:
// Let migrate store manage the dirty bit since dev DBs should be ephemeral anyways.
if _, err := schema.MigrateStore(ctx, schema.Dialect(dialect), b.DatabaseUrl); err != nil {
err = fmt.Errorf("error initializing store: %w", err)
if c != nil {
err = multierror.Append(err, c())
}
return err
}
}
b.InfoKeys = append(b.InfoKeys, "dev database url")
b.Info["dev database url"] = b.DatabaseUrl
if container != "" {
b.InfoKeys = append(b.InfoKeys, "dev database container")
b.Info["dev database container"] = strings.TrimPrefix(container, "/")
}
if err := b.ConnectToDatabase(ctx, dialect); err != nil {
if c != nil {
err = multierror.Append(err, c())
}
return err
}
b.Database.Config.Logger.LogMode(logger.Info)
if err := b.CreateGlobalKmsKeys(ctx); err != nil {
if c != nil {
err = multierror.Append(err, c())
}
return err
}
if _, err := b.CreateInitialLoginRole(ctx); err != nil {
if c != nil {
err = multierror.Append(err, c())
}
return err
}
if opts.withSkipAuthMethodCreation {
// now that we have passed all the error cases, reset c to be a noop so the
// defer doesn't do anything.
c = func() error { return nil }
return nil
}
if _, _, err := b.CreateInitialPasswordAuthMethod(ctx); err != nil {
return err
}
if !opts.withSkipOidcAuthMethodCreation {
if err := b.CreateDevOidcAuthMethod(ctx); err != nil {
return err
}
}
if opts.withSkipScopesCreation {
// now that we have passed all the error cases, reset c to be a noop so the
// defer doesn't do anything.
c = func() error { return nil }
return nil
}
if _, _, err := b.CreateInitialScopes(ctx); err != nil {
return err
}
if opts.withSkipHostResourcesCreation {
// now that we have passed all the error cases, reset c to be a noop so the
// defer doesn't do anything.
c = func() error { return nil }
return nil
}
if _, _, _, err := b.CreateInitialHostResources(context.Background()); err != nil {
return err
}
if opts.withSkipTargetCreation {
// now that we have passed all the error cases, reset c to be a noop so the
// defer doesn't do anything.
c = func() error { return nil }
return nil
}
if _, err := b.CreateInitialTarget(ctx); err != nil {
return err
}
// now that we have passed all the error cases, reset c to be a noop so the
// defer doesn't do anything.
c = func() error { return nil }
return nil
}
type oidcSetup struct {
clientId string
clientSecret oidc.ClientSecret
oidcPort int
callbackPort string
hostAddr string
authMethod *oidc.AuthMethod
pubKey []byte
privKey []byte
testProvider *capoidc.TestProvider
createUnpriv bool
callbackUrl *url.URL
}
func (b *Server) CreateDevOidcAuthMethod(ctx context.Context) error {
var err error
if b.DevOidcAuthMethodId == "" {
b.DevOidcAuthMethodId, err = db.NewPublicId(oidc.AuthMethodPrefix)
if err != nil {
return fmt.Errorf("error generating initial oidc auth method id: %w", err)
}
}
b.InfoKeys = append(b.InfoKeys, "generated oidc auth method id")
b.Info["generated oidc auth method id"] = b.DevOidcAuthMethodId
switch {
case b.DevUnprivilegedLoginName == "",
b.DevUnprivilegedPassword == "",
b.DevUnprivilegedUserId == "",
b.DevUnprivilegedOidcAccountId == "":
default:
b.DevOidcSetup.createUnpriv = true
}
// Trawl through the listeners and find the api listener so we can use the
// same host name/IP
{
for _, ln := range b.Listeners {
purpose := strings.ToLower(ln.Config.Purpose[0])
if purpose != "api" {
continue
}
b.DevOidcSetup.hostAddr, b.DevOidcSetup.callbackPort, err = net.SplitHostPort(ln.Config.Address)
if err != nil {
if strings.Contains(err.Error(), "missing port") {
b.DevOidcSetup.hostAddr = ln.Config.Address
// Use the default API port in the callback
b.DevOidcSetup.callbackPort = "9200"
} else {
return fmt.Errorf("error splitting host/port: %w", err)
}
}
}
if b.DevOidcSetup.hostAddr == "" {
return fmt.Errorf("could not determine address to use for built-in oidc dev listener")
}
}
// Find an available port -- allocate one, then close the listener, and
// re-use it. This is a sort of hacky way to get around the chicken and egg
// of the auth method needing to know the discovery URL and the test
// provider needing to know the callback URL.
l, err := net.Listen("tcp", net.JoinHostPort(b.DevOidcSetup.hostAddr, "0"))
if err != nil {
return fmt.Errorf("error finding port for oidc test provider: %w", err)
}
b.DevOidcSetup.oidcPort = l.(*net.TCPListener).Addr().(*net.TCPAddr).Port
if err := l.Close(); err != nil {
return fmt.Errorf("error closing initial test port: %w", err)
}
b.DevOidcSetup.callbackUrl, err = url.Parse(fmt.Sprintf("http://%s", net.JoinHostPort(b.DevOidcSetup.hostAddr, b.DevOidcSetup.callbackPort)))
if err != nil {
return fmt.Errorf("error parsing oidc test provider callback url: %w", err)
}
// Generate initial IDs/keys
{
b.DevOidcSetup.clientId, err = capoidc.NewID()
if err != nil {
return fmt.Errorf("unable to generate client id: %w", err)
}
clientSecret, err := capoidc.NewID()
if err != nil {
return fmt.Errorf("unable to generate client secret: %w", err)
}
b.DevOidcSetup.clientSecret = oidc.ClientSecret(clientSecret)
b.DevOidcSetup.pubKey, b.DevOidcSetup.privKey, err = ed25519.GenerateKey(nil)
if err != nil {
return fmt.Errorf("unable to generate signing key: %w", err)
}
}
// Create the subject information and testing provider
{
subInfo := map[string]*capoidc.TestSubject{
b.DevLoginName: {
Password: b.DevPassword,
UserInfo: map[string]interface{}{
"email": "admin@localhost",
"name": "Admin User",
},
},
}
if b.DevOidcSetup.createUnpriv {
subInfo[b.DevUnprivilegedLoginName] = &capoidc.TestSubject{
Password: b.DevUnprivilegedPassword,
UserInfo: map[string]interface{}{
"email": "user@localhost",
"name": "Unprivileged User",
},
}
}
clientSecret := string(b.DevOidcSetup.clientSecret)
b.DevOidcSetup.testProvider = capoidc.StartTestProvider(
&oidcLogger{},
capoidc.WithNoTLS(),
capoidc.WithTestHost(b.DevOidcSetup.hostAddr),
capoidc.WithTestPort(b.DevOidcSetup.oidcPort),
capoidc.WithTestDefaults(&capoidc.TestProviderDefaults{
CustomClaims: map[string]interface{}{
"mode": "dev",
},
SubjectInfo: subInfo,
SigningKey: &capoidc.TestSigningKey{
PrivKey: ed25519.PrivateKey(b.DevOidcSetup.privKey),
PubKey: ed25519.PublicKey(b.DevOidcSetup.pubKey),
Alg: capoidc.EdDSA,
},
AllowedRedirectURIs: []string{fmt.Sprintf("%s/v1/auth-methods/oidc:authenticate:callback", b.DevOidcSetup.callbackUrl.String())},
ClientID: &b.DevOidcSetup.clientId,
ClientSecret: &clientSecret,
}))
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
b.DevOidcSetup.testProvider.Stop()
return nil
})
}
// Create auth method and link accounts
{
b.DevOidcSetup.authMethod, err = b.createInitialOidcAuthMethod(ctx)
if err != nil {
return fmt.Errorf("error creating initial oidc auth method: %w", err)
}
}
return nil
}
func (b *Server) createInitialOidcAuthMethod(ctx context.Context) (*oidc.AuthMethod, error) {
rw := db.New(b.Database)
kmsRepo, err := kms.NewRepository(rw, rw)
if err != nil {
return nil, fmt.Errorf("error creating kms repository: %w", err)
}
kmsCache, err := kms.NewKms(kmsRepo)
if err != nil {
return nil, fmt.Errorf("error creating kms cache: %w", err)
}
if err := kmsCache.AddExternalWrappers(
kms.WithRootWrapper(b.RootKms),
); err != nil {
return nil, fmt.Errorf("error adding config keys to kms: %w", err)
}
discoveryUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", b.DevOidcSetup.hostAddr, b.DevOidcSetup.oidcPort))
if err != nil {
return nil, fmt.Errorf("error parsing oidc test provider address: %w", err)
}
// Create the auth method
oidcRepo, err := oidc.NewRepository(ctx, rw, rw, kmsCache)
if err != nil {
return nil, fmt.Errorf("error creating oidc repo: %w", err)
}
authMethod, err := oidc.NewAuthMethod(
ctx,
scope.Global.String(),
b.DevOidcSetup.clientId,
b.DevOidcSetup.clientSecret,
oidc.WithName("Generated global scope initial oidc auth method"),
oidc.WithDescription("Provides initial administrative and unprivileged authentication into Boundary"),
oidc.WithIssuer(discoveryUrl),
oidc.WithApiUrl(b.DevOidcSetup.callbackUrl),
oidc.WithSigningAlgs(oidc.EdDSA),
oidc.WithOperationalState(oidc.ActivePublicState))
if err != nil {
return nil, fmt.Errorf("error creating new in memory oidc auth method: %w", err)
}
if b.DevOidcAuthMethodId == "" {
b.DevOidcAuthMethodId, err = db.NewPublicId(oidc.AuthMethodPrefix)
if err != nil {
return nil, fmt.Errorf("error generating initial oidc auth method id: %w", err)
}
}
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
select {
case <-b.ShutdownCh:
cancel()
case <-cancelCtx.Done():
}
}()
b.DevOidcSetup.authMethod, err = oidcRepo.CreateAuthMethod(
cancelCtx,
authMethod,
oidc.WithPublicId(b.DevOidcAuthMethodId))
if err != nil {
return nil, fmt.Errorf("error saving oidc auth method to the db: %w", err)
}
// Create accounts
{
createAndLinkAccount := func(loginName, userId, accountId, typ string) error {
acct, err := oidc.NewAccount(
ctx,
b.DevOidcSetup.authMethod.GetPublicId(),
loginName,
oidc.WithDescription(fmt.Sprintf("Initial %s OIDC account", typ)),
)
if err != nil {
return fmt.Errorf("error generating %s oidc account: %w", typ, err)
}
acct, err = oidcRepo.CreateAccount(
cancelCtx,
b.DevOidcSetup.authMethod.GetScopeId(),
acct,
oidc.WithPublicId(accountId),
)
if err != nil {
return fmt.Errorf("error creating %s oidc account: %w", typ, err)
}
// Link accounts to existing user
iamRepo, err := iam.NewRepository(rw, rw, kmsCache)
if err != nil {
return fmt.Errorf("unable to create iam repo: %w", err)
}
u, _, err := iamRepo.LookupUser(cancelCtx, userId)
if err != nil {
return fmt.Errorf("error looking up %s user: %w", typ, err)
}
if _, err = iamRepo.AddUserAccounts(cancelCtx, u.GetPublicId(), u.GetVersion(), []string{acct.GetPublicId()}); err != nil {
return fmt.Errorf("error associating initial %s user with account: %w", typ, err)
}
return nil
}
if err := createAndLinkAccount(b.DevLoginName, b.DevUserId, b.DevOidcAccountId, "admin"); err != nil {
return nil, err
}
if b.DevOidcSetup.createUnpriv {
if err := createAndLinkAccount(b.DevUnprivilegedLoginName, b.DevUnprivilegedUserId, b.DevUnprivilegedOidcAccountId, "unprivileged"); err != nil {
return nil, err
}
}
}
return nil, nil
}
// oidcLogger satisfies the interface requirements for the oidc.TestProvider logger.
type oidcLogger struct {
Ctx context.Context // nil ctx is allowed/okay
}
// Errorf will use the sys eventer to emit an error event
func (l *oidcLogger) Errorf(format string, args ...interface{}) {
event.WriteError(l.Ctx, l.caller(), fmt.Errorf(format, args...))
}
// Infof will use the sys eventer to emit an system event
func (l *oidcLogger) Infof(format string, args ...interface{}) {
event.WriteSysEvent(l.Ctx, l.caller(), fmt.Sprintf(format, args...))
}
// FailNow will panic (as required by the interface it's implementing)
func (_ *oidcLogger) FailNow() {
panic("sys eventer failed, see logs for output (if any)")
}
func (_ *oidcLogger) caller() event.Op {
var caller event.Op
pc, _, _, ok := runtime.Caller(2)
details := runtime.FuncForPC(pc)
if ok && details != nil {
caller = event.Op(details.Name())
} else {
caller = "unknown operation"
}
return caller
}