diff --git a/builtin/logical/postgresql/backend.go b/builtin/logical/postgresql/backend.go index 75c2c315ff..d101c2e9e6 100644 --- a/builtin/logical/postgresql/backend.go +++ b/builtin/logical/postgresql/backend.go @@ -27,6 +27,7 @@ func Backend() *framework.Backend { Paths: []*framework.Path{ pathConfigConnection(&b), + pathConfigLease(&b), pathRoles(&b), pathRoleCreate(&b), }, @@ -95,6 +96,24 @@ func (b *backend) ResetDB() { b.db = nil } +// Lease returns the lease information +func (b *backend) Lease(s logical.Storage) (*configLease, error) { + entry, err := s.Get("config/lease") + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result configLease + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + const backendHelp = ` The PostgreSQL backend dynamically generates database users. diff --git a/builtin/logical/postgresql/backend_test.go b/builtin/logical/postgresql/backend_test.go index 521c651387..4dbf53e06f 100644 --- a/builtin/logical/postgresql/backend_test.go +++ b/builtin/logical/postgresql/backend_test.go @@ -11,9 +11,11 @@ import ( ) func TestBackend_basic(t *testing.T) { + b := Backend() + logicaltest.Test(t, logicaltest.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - Backend: Backend(), + Backend: b, Steps: []logicaltest.TestStep{ testAccStepConfig(t), testAccStepRole(t), @@ -70,5 +72,6 @@ func testAccStepReadCreds(t *testing.T, name string) logicaltest.TestStep { const testRole = ` CREATE ROLE "{{name}}" WITH LOGIN - PASSWORD '{{password}}'; + PASSWORD '{{password}}' + VALID UNTIL '{{expiration}}'; ` diff --git a/builtin/logical/postgresql/path_config.go b/builtin/logical/postgresql/path_config_connection.go similarity index 100% rename from builtin/logical/postgresql/path_config.go rename to builtin/logical/postgresql/path_config_connection.go diff --git a/builtin/logical/postgresql/path_config_lease.go b/builtin/logical/postgresql/path_config_lease.go new file mode 100644 index 0000000000..e13bb20891 --- /dev/null +++ b/builtin/logical/postgresql/path_config_lease.go @@ -0,0 +1,83 @@ +package postgresql + +import ( + "fmt" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfigLease(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "config/lease", + Fields: map[string]*framework.FieldSchema{ + "lease": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Default lease for roles.", + }, + + "lease_max": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Maximum time a credential is valid for.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathLeaseWrite, + }, + + HelpSynopsis: pathConfigLeaseHelpSyn, + HelpDescription: pathConfigLeaseHelpDesc, + } +} + +func (b *backend) pathLeaseWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + leaseRaw := d.Get("lease").(string) + leaseMaxRaw := d.Get("lease").(string) + + lease, err := time.ParseDuration(leaseRaw) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid lease: %s", err)), nil + } + leaseMax, err := time.ParseDuration(leaseMaxRaw) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid lease: %s", err)), nil + } + + // Store it + entry, err := logical.StorageEntryJSON("config/lease", &configLease{ + Lease: lease, + LeaseMax: leaseMax, + }) + if err != nil { + return nil, err + } + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +type configLease struct { + Lease time.Duration + LeaseMax time.Duration +} + +const pathConfigLeaseHelpSyn = ` +Configure the default lease information for generated credentials. +` + +const pathConfigLeaseHelpDesc = ` +This configures the default lease information used for credentials +generated by this backend. The lease specifies the duration that a +credential will be valid for, as well as the maximum session for +a set of credentials. + +The format for the lease is "1h" or integer and then unit. The longest +unit is hour. +` diff --git a/builtin/logical/postgresql/path_role_create.go b/builtin/logical/postgresql/path_role_create.go index 9feefcab23..52a1dc31ae 100644 --- a/builtin/logical/postgresql/path_role_create.go +++ b/builtin/logical/postgresql/path_role_create.go @@ -48,6 +48,15 @@ func (b *backend) pathRoleCreateRead( return nil, err } + // Determine if we have a lease + lease, err := b.Lease(req.Storage) + if err != nil { + return nil, err + } + if lease == nil { + lease = &configLease{Lease: 1 * time.Hour} + } + // Get our connection db, err := b.DB(req.Storage) if err != nil { @@ -59,10 +68,13 @@ func (b *backend) pathRoleCreateRead( "vault-%s-%d-%d", req.DisplayName, time.Now().Unix(), rand.Int31n(10000)) password := generateUUID() + expiration := time.Now().UTC(). + Add(lease.Lease + time.Duration((float64(lease.Lease) * 0.1))). + Format("2006-01-02 15:04:05") query := Query(role.SQL, map[string]string{ "name": username, "password": password, - "expiration": "", + "expiration": expiration, }) // Prepare the statement and execute it @@ -76,12 +88,14 @@ func (b *backend) pathRoleCreateRead( } // Return the secret - return b.Secret(SecretCredsType).Response(map[string]interface{}{ + resp := b.Secret(SecretCredsType).Response(map[string]interface{}{ "username": username, "password": password, }, map[string]interface{}{ "username": username, - }), nil + }) + resp.Secret.Lease = lease.Lease + return resp, nil } const pathRoleCreateReadHelpSyn = `