Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OSS stub functions for Self-Managed Static Roles #28199

Merged
merged 6 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions builtin/credential/aws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import (
"strconv"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
Expand Down
3 changes: 1 addition & 2 deletions builtin/credential/cert/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ import (
"net/url"
"strings"

"github.com/hashicorp/vault/sdk/helper/locksutil"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/cidrutil"
"github.com/hashicorp/vault/sdk/helper/locksutil"
"github.com/hashicorp/vault/sdk/helper/ocsp"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
Expand Down
15 changes: 15 additions & 0 deletions builtin/logical/database/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ func staticFields() map[string]*framework.FieldSchema {
this functionality. See the plugin's API page for more information on
support and formatting for this parameter.`,
},
"self_managed_password": {
Type: framework.TypeString,
Description: `Used to connect to a self-managed static account. Must
be provided by the user when root credentials are not provided.`,
},
}
return fields
}
Expand Down Expand Up @@ -628,6 +633,10 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
}
}

if smPasswordRaw, ok := data.GetOk("self_managed_password"); ok && createRole {
role.StaticAccount.SelfManagedPassword = smPasswordRaw.(string)
}

var credentialConfig map[string]string
if raw, ok := data.GetOk("credential_config"); ok {
credentialConfig = raw.(map[string]string)
Expand Down Expand Up @@ -785,6 +794,12 @@ type staticAccount struct {
// Username to create or assume management for static accounts
Username string `json:"username"`

// SelfManagedPassword is used to make a dedicated connection to the DB
// user specified by Username. The credentials will leverage the existing
// static role mechanisms to handle password rotations. Required when root
// credentials are not provided.
SelfManagedPassword string `json:"self_managed_password"`

// Password is the current password credential for static accounts. As an input,
// this is used/required when trying to assume management of an existing static
// account. Returned on credential request if the role's credential type is
Expand Down
11 changes: 11 additions & 0 deletions builtin/logical/database/rotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag
Commands: input.Role.Statements.Rotation,
}

// Add external password to request so we can use static account connection
if input.Role.StaticAccount.SelfManagedPassword != "" {
updateReq.SelfManagedPassword = input.Role.StaticAccount.SelfManagedPassword
}

// Use credential from input if available. This happens if we're restoring from
// a WAL item or processing the rotation queue with an item that has a WAL
// associated with it
Expand Down Expand Up @@ -529,6 +534,12 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag
}
modified = true

// static user password successfully updated in external system
// update self-managed password if available for future connections
if input.Role.StaticAccount.SelfManagedPassword != "" {
input.Role.StaticAccount.SelfManagedPassword = input.Role.StaticAccount.Password
}

// Store updated role information
// lvr is the known LastVaultRotation
lvr := time.Now()
Expand Down
3 changes: 3 additions & 0 deletions changelog/28199.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**Self-Managed Static Roles**: Self-Managed Static Roles are now supported for select SQL database engines (Postgres, Oracle). Requires Vault Enterprise.
```
23 changes: 23 additions & 0 deletions helper/testhelpers/postgresql/postgresqlhelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func PrepareTestContainer(t *testing.T) (func(), string) {
return cleanup, url
}

func PrepareTestContainerSelfManaged(t *testing.T) (func(), *url.URL) {
return prepareTestContainerSelfManaged(t, defaultRunOpts(t), defaultPGPass, true, false, false)
}

func PrepareTestContainerMultiHost(t *testing.T) (func(), string) {
_, cleanup, url, _ := prepareTestContainer(t, defaultRunOpts(t), defaultPGPass, true, false, true)

Expand Down Expand Up @@ -198,6 +202,25 @@ func prepareTestContainer(t *testing.T, runOpts docker.RunOptions, password stri
return runner, svc.Cleanup, svc.Config.URL().String(), containerID
}

func prepareTestContainerSelfManaged(t *testing.T, runOpts docker.RunOptions, password string, addSuffix, forceLocalAddr, useFallback bool,
) (func(), *url.URL) {
if os.Getenv("PG_URL") != "" {
return func() {}, nil
}

runner, err := docker.NewServiceRunner(runOpts)
if err != nil {
t.Fatalf("Could not start docker Postgres: %s", err)
}

svc, _, err := runner.StartNewService(context.Background(), addSuffix, forceLocalAddr, connectPostgres(password, runOpts.ImageRepo, useFallback))
if err != nil {
t.Fatalf("Could not start docker Postgres: %s", err)
}

return svc.Cleanup, svc.Config.URL()
}

func getPostgresSSLConfig(t *testing.T, host, sslMode, caCert, clientCert, clientKey string, useFallback bool) docker.ServiceConfig {
if useFallback {
// set the first host to a bad address so we can test the fallback logic
Expand Down
47 changes: 37 additions & 10 deletions plugins/database/postgresql/postgresql.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ func (p *PostgreSQL) getConnection(ctx context.Context) (*sql.DB, error) {
return db.(*sql.DB), nil
}

func (p *PostgreSQL) getStaticConnection(ctx context.Context, username, password string) (*sql.DB, error) {
db, err := p.StaticConnection(ctx, username, password)
if err != nil {
return nil, err
}

return db, nil
}

func (p *PostgreSQL) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) {
if req.Username == "" {
return dbplugin.UpdateUserResponse{}, fmt.Errorf("missing username")
Expand All @@ -209,17 +218,17 @@ func (p *PostgreSQL) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequ

merr := &multierror.Error{}
if req.Password != nil {
err := p.changeUserPassword(ctx, req.Username, req.Password)
err := p.changeUserPassword(ctx, req.Username, req.Password, req.SelfManagedPassword)
merr = multierror.Append(merr, err)
}
if req.Expiration != nil {
err := p.changeUserExpiration(ctx, req.Username, req.Expiration)
err := p.changeUserExpiration(ctx, req.Username, req.Expiration, req.SelfManagedPassword)
merr = multierror.Append(merr, err)
}
return dbplugin.UpdateUserResponse{}, merr.ErrorOrNil()
}

func (p *PostgreSQL) changeUserPassword(ctx context.Context, username string, changePass *dbplugin.ChangePassword) error {
func (p *PostgreSQL) changeUserPassword(ctx context.Context, username string, changePass *dbplugin.ChangePassword, selfManagedPass string) error {
stmts := changePass.Statements.Commands
if len(stmts) == 0 {
stmts = []string{defaultChangePasswordStatement}
Expand All @@ -233,9 +242,18 @@ func (p *PostgreSQL) changeUserPassword(ctx context.Context, username string, ch
p.Lock()
defer p.Unlock()

db, err := p.getConnection(ctx)
if err != nil {
return fmt.Errorf("unable to get connection: %w", err)
var db *sql.DB
var err error
if selfManagedPass == "" {
db, err = p.getConnection(ctx)
if err != nil {
return fmt.Errorf("unable to get connection: %w", err)
}
} else {
db, err = p.getStaticConnection(ctx, username, selfManagedPass)
if err != nil {
return fmt.Errorf("unable to get static connection from cache: %w", err)
}
}

// Check if the role exists
Expand Down Expand Up @@ -285,7 +303,7 @@ func (p *PostgreSQL) changeUserPassword(ctx context.Context, username string, ch
return nil
}

func (p *PostgreSQL) changeUserExpiration(ctx context.Context, username string, changeExp *dbplugin.ChangeExpiration) error {
func (p *PostgreSQL) changeUserExpiration(ctx context.Context, username string, changeExp *dbplugin.ChangeExpiration, selfManagedPass string) error {
p.Lock()
defer p.Unlock()

Expand All @@ -294,9 +312,18 @@ func (p *PostgreSQL) changeUserExpiration(ctx context.Context, username string,
renewStmts = []string{defaultExpirationStatement}
}

db, err := p.getConnection(ctx)
if err != nil {
return err
var db *sql.DB
var err error
if selfManagedPass == "" {
db, err = p.getConnection(ctx)
if err != nil {
return fmt.Errorf("unable to get connection: %w", err)
}
} else {
db, err = p.getStaticConnection(ctx, username, selfManagedPass)
if err != nil {
return fmt.Errorf("unable to get static connection from cache: %w", err)
}
}

tx, err := db.BeginTx(ctx, nil)
Expand Down
119 changes: 119 additions & 0 deletions plugins/database/postgresql/postgresql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,94 @@ func TestPostgreSQL_Initialize_CloudGCP(t *testing.T) {
}
}

// TestPostgreSQL_Initialize_SelfManaged_OSS tests the initialization of
// the self-managed flow and ensures an error is returned on OSS.
func TestPostgreSQL_Initialize_SelfManaged_OSS(t *testing.T) {
cleanup, url := postgresql.PrepareTestContainerSelfManaged(t)
defer cleanup()

connURL := fmt.Sprintf("postgresql://{{username}}:{{password}}@%s/postgres?sslmode=disable", url.Host)

testCases := []struct {
name string
connectionDetails map[string]interface{}
wantErr bool
errContains string
}{
{
name: "no parameters set",
connectionDetails: map[string]interface{}{
"connection_url": connURL,
"self_managed": false,
"username": "",
"password": "",
},
wantErr: true,
errContains: "must either provide username/password or set self-managed to 'true'",
},
{
name: "both sets of parameters set",
connectionDetails: map[string]interface{}{
"connection_url": connURL,
"self_managed": true,
"username": "test",
"password": "test",
},
wantErr: true,
errContains: "cannot use both self-managed and vault-managed workflows",
},
{
name: "either username/password with self-managed",
connectionDetails: map[string]interface{}{
"connection_url": connURL,
"self_managed": true,
"username": "test",
"password": "",
},
wantErr: true,
errContains: "cannot use both self-managed and vault-managed workflows",
},
{
name: "cache not implemented",
connectionDetails: map[string]interface{}{
"connection_url": connURL,
"self_managed": true,
"username": "",
"password": "",
},
wantErr: true,
errContains: "self-managed static roles only available in Vault Enterprise",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := dbplugin.InitializeRequest{
Config: tc.connectionDetails,
VerifyConnection: true,
}

db := new()
_, err := dbtesting.VerifyInitialize(t, db, req)
if err == nil && tc.wantErr {
t.Fatalf("got: %s, wantErr: %t", err, tc.wantErr)
}

if err != nil && !strings.Contains(err.Error(), tc.errContains) {
t.Fatalf("expected error: %s, received error: %s", tc.errContains, err)
}

if !tc.wantErr && !db.Initialized {
t.Fatal("Database should be initialized")
}

if err := db.Close(); err != nil {
t.Fatalf("err closing DB: %s", err)
}
})
}
}

// TestPostgreSQL_PasswordAuthentication tests that the default "password_authentication" is "none", and that
// an error is returned if an invalid "password_authentication" is provided.
func TestPostgreSQL_PasswordAuthentication(t *testing.T) {
Expand Down Expand Up @@ -1045,6 +1133,37 @@ func TestUpdateUser_Password(t *testing.T) {
})
}

// TestUpdateUser_SelfManaged_OSS checks basic validation
// for self-managed fields and confirms an error is returned on OSS
func TestUpdateUser_SelfManaged_OSS(t *testing.T) {
// Shared test container for speed - there should not be any overlap between the tests
db, cleanup := getPostgreSQL(t, nil)
defer cleanup()

updateReq := dbplugin.UpdateUserRequest{
Username: "static",
Password: &dbplugin.ChangePassword{
NewPassword: "somenewpassword",
Statements: dbplugin.Statements{
Commands: nil,
},
},
SelfManagedPassword: "test",
}

expectedErr := "self-managed static roles only available in Vault Enterprise"

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.UpdateUser(ctx, updateReq)
if err == nil {
t.Fatalf("err expected, got nil")
}
if !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("err expected: %s, got: %s", expectedErr, err)
}
}

func TestUpdateUser_Expiration(t *testing.T) {
type testCase struct {
initialExpiration time.Time
Expand Down
2 changes: 2 additions & 0 deletions sdk/database/dbplugin/v5/conversions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func TestConversionsHaveAllFields(t *testing.T) {
},
},
},
SelfManagedPassword: "test-password",
}

protoReq, err := updateUserReqToProto(req)
Expand Down Expand Up @@ -194,6 +195,7 @@ func TestConversionsHaveAllFields(t *testing.T) {
},
},
},
SelfManagedPassword: "test-password",
}

protoReq, err := getUpdateUserRequest(req)
Expand Down
7 changes: 7 additions & 0 deletions sdk/database/dbplugin/v5/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ type UpdateUserRequest struct {
// Expiration indicates the new expiration date to change to.
// If nil, no change is requested.
Expiration *ChangeExpiration

// SelfManagedPassword is the password for an externally managed user in the DB.
// If this field is supplied, a DB connection is retrieved from the static
// account cache for the particular DB plugin and used to update the password of
// the self-managed static role.
// *ENTERPRISE-ONLY*
SelfManagedPassword string
}

// ChangePublicKey of a given user
Expand Down
11 changes: 6 additions & 5 deletions sdk/database/dbplugin/v5/grpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,12 @@ func updateUserReqToProto(req UpdateUserRequest) (*proto.UpdateUserRequest, erro
}

rpcReq := &proto.UpdateUserRequest{
Username: req.Username,
CredentialType: int32(req.CredentialType),
Password: password,
PublicKey: publicKey,
Expiration: expiration,
Username: req.Username,
CredentialType: int32(req.CredentialType),
Password: password,
PublicKey: publicKey,
Expiration: expiration,
SelfManagedPassword: req.SelfManagedPassword,
}
return rpcReq, nil
}
Expand Down
Loading
Loading