* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * Update copyright file headers to BUS-1.1 * Rerun make gen This will pick up the last of the license changes * Revert "Rerun make gen" This reverts commit dbe0968bffd92745d8663c6940debae098395a6b. * Remove copywrite from generation script The current version of copywrite is not sophisticated enough to only apply the copywrite changes to certain subdirectories, so to avoid accidentally relicensing our api and sdk modules, we will stop running the copywrite script until such time that it can do the right thing. * License protobuf API as MPL The protobuf API files are used to generate the SDK, since we want the SDK to be MPL, the protobuf files must also be MPL. This extends to all protobuf files whose sources end up in the SDK directory, including: - internal/proto/controller/api - internal/proto/controller/custom_options - internal/proto/plugin It also includes the generated service directory in internal/gen/controller/api/services, since they are generated from the protobuf sources used for the SDK. * Change website license file name This makes the naming scheme consistent with the rest of the repository. * Add note on licensing to README * Set copywrite license to BUSL * Revert "Add note on licensing to README" This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe. --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
694 lines
21 KiB
Go
694 lines
21 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package base
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/auth/ldap"
|
|
"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"
|
|
"github.com/jimlambrt/gldap"
|
|
"github.com/jimlambrt/gldap/testdirectory"
|
|
)
|
|
|
|
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.OpenAndSetServerDatabase(ctx, dialect); err != nil {
|
|
if c != nil {
|
|
err = multierror.Append(err, c())
|
|
}
|
|
return err
|
|
}
|
|
|
|
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.withSkipLdapAuthMethodCreation {
|
|
if err := b.CreateDevLdapAuthMethod(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.CreateInitialTargetWithAddress(ctx); err != nil {
|
|
return err
|
|
}
|
|
if _, err := b.CreateInitialTargetWithHostSources(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 ldapSetup struct {
|
|
testDirectory *testdirectory.Directory
|
|
authMethod *ldap.AuthMethod
|
|
}
|
|
|
|
func (b *Server) CreateDevLdapAuthMethod(ctx context.Context) error {
|
|
var (
|
|
err error
|
|
port int
|
|
host string
|
|
createUnpriv bool
|
|
)
|
|
|
|
if b.DevLdapAuthMethodId == "" {
|
|
b.DevLdapAuthMethodId, err = db.NewPublicId(ctx, globals.LdapAuthMethodPrefix)
|
|
if err != nil {
|
|
return fmt.Errorf("error generating initial ldap auth method id: %w", err)
|
|
}
|
|
}
|
|
b.InfoKeys = append(b.InfoKeys, "generated ldap auth method id")
|
|
b.Info["generated ldap auth method id"] = b.DevLdapAuthMethodId
|
|
|
|
switch {
|
|
case b.DevUnprivilegedLoginName == "",
|
|
b.DevUnprivilegedPassword == "",
|
|
b.DevUnprivilegedUserId == "",
|
|
b.DevUnprivilegedOidcAccountId == "":
|
|
|
|
default:
|
|
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
|
|
}
|
|
host, _, err = net.SplitHostPort(ln.Config.Address)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "missing port") {
|
|
host = ln.Config.Address
|
|
} else {
|
|
return fmt.Errorf("error splitting host/port: %w", err)
|
|
}
|
|
}
|
|
}
|
|
if host == "" {
|
|
return fmt.Errorf("could not determine address to use for built-in oidc dev listener")
|
|
}
|
|
}
|
|
|
|
tb := &oidcLogger{}
|
|
|
|
port = testdirectory.FreePort(tb)
|
|
b.DevLdapSetup.testDirectory = testdirectory.Start(tb,
|
|
testdirectory.WithNoTLS(tb),
|
|
testdirectory.WithHost(tb, host),
|
|
testdirectory.WithPort(tb, port),
|
|
testdirectory.WithDefaults(tb, &testdirectory.Defaults{AllowAnonymousBind: true}),
|
|
)
|
|
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
|
|
b.DevLdapSetup.testDirectory.Stop()
|
|
return nil
|
|
})
|
|
b.InfoKeys = append(b.InfoKeys, "generated ldap auth method host:port")
|
|
b.Info["generated ldap auth method host:port"] = fmt.Sprintf("%s:%d (does not have a root DSE; use simple bind)", host, port)
|
|
|
|
// users="ou=people,dc=example,dc=org" groups="ou=groups,dc=example,dc=org"
|
|
b.InfoKeys = append(b.InfoKeys, "generated ldap auth method base search DNs")
|
|
b.Info["generated ldap auth method base search DNs"] = `users="ou=people,dc=example,dc=org" groups="ou=groups,dc=example,dc=org"`
|
|
|
|
groups := []*gldap.Entry{
|
|
testdirectory.NewGroup(tb, "admin", []string{"admin"}),
|
|
}
|
|
|
|
createUserFn := func(userName, passwd string, withMembersOf []string) *gldap.Entry {
|
|
entryAttrs := map[string][]string{
|
|
"name": {userName},
|
|
"email": {fmt.Sprintf("%s@localhost", userName)},
|
|
"password": {passwd},
|
|
}
|
|
if len(withMembersOf) > 0 {
|
|
entryAttrs["memberOf"] = withMembersOf
|
|
}
|
|
DN := fmt.Sprintf("%s=%s,%s", testdirectory.DefaultUserAttr, userName, testdirectory.DefaultUserDN)
|
|
return gldap.NewEntry(
|
|
DN,
|
|
entryAttrs,
|
|
)
|
|
}
|
|
users := []*gldap.Entry{
|
|
createUserFn(b.DevLoginName, b.DevPassword, []string{"admin"}),
|
|
}
|
|
|
|
if createUnpriv {
|
|
users = append(users, createUserFn(b.DevUnprivilegedLoginName, b.DevUnprivilegedPassword, nil))
|
|
}
|
|
b.DevLdapSetup.testDirectory.SetUsers(users...)
|
|
b.DevLdapSetup.testDirectory.SetGroups(groups...)
|
|
|
|
// Create auth method and link accounts
|
|
{
|
|
b.DevLdapSetup.authMethod, err = b.createInitialLdapAuthMethod(ctx, host, port, createUnpriv)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating initial ldap auth method: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Server) createInitialLdapAuthMethod(ctx context.Context, host string, port int, createUnprivAccount bool) (*ldap.AuthMethod, error) {
|
|
rw := db.New(b.Database)
|
|
kmsCache, err := kms.New(ctx, rw, rw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating kms cache: %w", err)
|
|
}
|
|
if err := kmsCache.AddExternalWrappers(
|
|
b.Context,
|
|
kms.WithRootWrapper(b.RootKms),
|
|
); err != nil {
|
|
return nil, fmt.Errorf("error adding config keys to kms: %w", err)
|
|
}
|
|
ldapRepo, err := ldap.NewRepository(ctx, rw, rw, kmsCache)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating ldap repo: %w", err)
|
|
}
|
|
|
|
u, err := url.Parse(fmt.Sprintf("ldap://%s:%d", host, port))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating ldap url: %w", err)
|
|
}
|
|
authMethod, err := ldap.NewAuthMethod(
|
|
ctx,
|
|
scope.Global.String(),
|
|
ldap.WithUrls(ctx, u),
|
|
ldap.WithName(ctx, "Generated global scope initial ldap auth method"),
|
|
ldap.WithDescription(ctx, "Provides initial administrative and unprivileged authentication into Boundary"),
|
|
ldap.WithDiscoverDn(ctx),
|
|
ldap.WithUserDn(ctx, testdirectory.DefaultUserDN),
|
|
ldap.WithGroupDn(ctx, testdirectory.DefaultGroupDN),
|
|
ldap.WithOperationalState(ctx, ldap.ActivePublicState),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating new in memory ldap auth method: %w", err)
|
|
}
|
|
if b.DevLdapAuthMethodId == "" {
|
|
b.DevLdapAuthMethodId, err = db.NewPublicId(ctx, globals.LdapAuthMethodPrefix)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error generating initial ldap auth method id: %w", err)
|
|
}
|
|
}
|
|
|
|
createdAuthMethod, err := ldapRepo.CreateAuthMethod(ctx, authMethod, ldap.WithPublicId(ctx, b.DevLdapAuthMethodId))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error saving ldap auth method: %w", err)
|
|
}
|
|
|
|
// create dev ldap accounts
|
|
{
|
|
createAndLinkAccount := func(loginName, userId, typ string) error {
|
|
acct, err := ldap.NewAccount(
|
|
ctx,
|
|
createdAuthMethod.GetScopeId(),
|
|
createdAuthMethod.GetPublicId(),
|
|
loginName,
|
|
ldap.WithDescription(ctx, fmt.Sprintf("Initial %s ldap account", typ)),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error generating %s ldap account: %w", typ, err)
|
|
}
|
|
acct, err = ldapRepo.CreateAccount(ctx, acct)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating %s ldap account: %w", typ, err)
|
|
}
|
|
|
|
// Link accounts to existing user
|
|
iamRepo, err := iam.NewRepository(ctx, rw, rw, kmsCache)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create iam repo: %w", err)
|
|
}
|
|
|
|
u, _, err := iamRepo.LookupUser(ctx, userId)
|
|
if err != nil {
|
|
return fmt.Errorf("error looking up %s user: %w", typ, err)
|
|
}
|
|
if _, err = iamRepo.AddUserAccounts(ctx, 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, "admin"); err != nil {
|
|
return nil, err
|
|
}
|
|
if createUnprivAccount {
|
|
if err := createAndLinkAccount(b.DevUnprivilegedLoginName, b.DevUnprivilegedUserId, "unprivileged"); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
}
|
|
return createdAuthMethod, 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(ctx, globals.OidcAuthMethodPrefix)
|
|
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]any{
|
|
"email": "admin@localhost",
|
|
"name": "Admin User",
|
|
},
|
|
},
|
|
}
|
|
if b.DevOidcSetup.createUnpriv {
|
|
subInfo[b.DevUnprivilegedLoginName] = &capoidc.TestSubject{
|
|
Password: b.DevUnprivilegedPassword,
|
|
UserInfo: map[string]any{
|
|
"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]any{
|
|
"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)
|
|
|
|
kmsCache, err := kms.New(ctx, rw, rw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating kms cache: %w", err)
|
|
}
|
|
if err := kmsCache.AddExternalWrappers(
|
|
b.Context,
|
|
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(ctx, globals.OidcAuthMethodPrefix)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error generating initial oidc auth method id: %w", err)
|
|
}
|
|
}
|
|
|
|
b.DevOidcSetup.authMethod, err = oidcRepo.CreateAuthMethod(
|
|
ctx,
|
|
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(
|
|
ctx,
|
|
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(ctx, rw, rw, kmsCache)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create iam repo: %w", err)
|
|
}
|
|
|
|
u, _, err := iamRepo.LookupUser(ctx, userId)
|
|
if err != nil {
|
|
return fmt.Errorf("error looking up %s user: %w", typ, err)
|
|
}
|
|
if _, err = iamRepo.AddUserAccounts(ctx, 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 ...any) {
|
|
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 ...any) {
|
|
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
|
|
}
|
|
|
|
func (l *oidcLogger) Log(args ...interface{}) {
|
|
event.WriteSysEvent(l.Ctx, l.caller(), fmt.Sprintf("%v", args...))
|
|
}
|