From d969399f646e788faa398ae17fa31e05f4c88460 Mon Sep 17 00:00:00 2001 From: Jared Petersen Date: Fri, 13 May 2022 01:14:38 -0600 Subject: [PATCH] Add lease information to DB credentials (#7) --- CHANGELOG.md | 4 ++++ db/db.go | 19 ++++++++++++++++++- db/db_test.go | 52 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c9c33c..d9cd162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.4] - 2022-05-13 +### Added +- Lease information to Database credentials so that you know when the secret expires + ## [0.0.3] - 2022-05-05 ### Changed - vaultx client uses functions for nested client gateways instead of struct fields for improved testability diff --git a/db/db.go b/db/db.go index c39d152..0927254 100644 --- a/db/db.go +++ b/db/db.go @@ -3,6 +3,7 @@ package db import ( "context" "fmt" + "time" "github.com/jaredpetersen/vaultx/api" "github.com/jaredpetersen/vaultx/auth" @@ -20,6 +21,14 @@ type Client struct { type Credentials struct { Username string Password string + Lease Lease +} + +// Lease contains information about how long the secret is valid. +type Lease struct { + ID string + Renewable bool + Expiration time.Duration } const httpPathDBCredentials = "/v1/database/creds/" @@ -32,7 +41,10 @@ func (db *Client) GenerateCredentials(ctx context.Context, role string) (*Creden } type credentialsResponseWrapper struct { - Data credentialsResponse `json:"data"` + LeaseID string `json:"lease_id"` + LaseDuration int `json:"lease_duration"` + Renewable bool `json:"renewable"` + Data credentialsResponse `json:"data"` } res, err := db.API.Read(ctx, httpPathDBCredentials+role, db.TokenManager.GetToken().Value) @@ -53,6 +65,11 @@ func (db *Client) GenerateCredentials(ctx context.Context, role string) (*Creden credentials := Credentials{ Username: resBody.Data.Username, Password: resBody.Data.Password, + Lease: Lease{ + ID: resBody.LeaseID, + Renewable: resBody.Renewable, + Expiration: time.Duration(resBody.LaseDuration) * time.Second, + }, } return &credentials, nil diff --git a/db/db_test.go b/db/db_test.go index de63ccf..6d7d6fb 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -7,8 +7,10 @@ import ( "io" "strings" "testing" + "time" "github.com/hashicorp/go-cleanhttp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/jaredpetersen/vaultx/api" @@ -30,12 +32,27 @@ func TestGenerateCredentialsReturnsCredentials(t *testing.T) { tokenManager := authmocks.TokenManager{} tokenManager.On("GetToken").Return(token, nil) + generatedLeaseID := "somelease" + generatedLeaseExpiration := 400 + generatedLeaseRenewable := true generatedUsername := "someusername" generatedPassword := "somepassword" // Set up mocked API response + resBodyFmt := `{ + "lease_id": "%s", + "lease_duration": %d, + "renewable": %t, + "data": { + "username": "%s", + "password": "%s" + } + }` resBody := fmt.Sprintf( - "{\"data\": {\"username\": \"%s\", \"password\":\"%s\"}}", + resBodyFmt, + generatedLeaseID, + generatedLeaseExpiration, + generatedLeaseRenewable, generatedUsername, generatedPassword) res := api.Response{ @@ -57,10 +74,11 @@ func TestGenerateCredentialsReturnsCredentials(t *testing.T) { dbCredentials, err := dbc.GenerateCredentials(ctx, dbRole) require.NoError(t, err, "Credential generation failure") require.NotEmpty(t, dbCredentials, "Credentials are empty") - require.NotEmpty(t, dbCredentials.Username, "Username is empty") - require.Equal(t, generatedUsername, dbCredentials.Username, "Username matches original credentials") - require.NotEmpty(t, dbCredentials.Password, "Password is empty") - require.Equal(t, generatedPassword, dbCredentials.Password, "Password matches original credentials") + assert.Equal(t, generatedUsername, dbCredentials.Username, "Username is incorrect") + assert.Equal(t, generatedPassword, dbCredentials.Password, "Password is incorrect") + assert.Equal(t, generatedLeaseID, dbCredentials.Lease.ID, "Lease ID is incorrect") + assert.Equal(t, generatedLeaseRenewable, dbCredentials.Lease.Renewable, "Lease renewable is incorrect") + assert.Equal(t, time.Duration(generatedLeaseExpiration)*time.Second, dbCredentials.Lease.Expiration, "Lease expiration is incorrect") } func TestGenerateCredentialsReturnsErrorOnRequestFailure(t *testing.T) { @@ -88,7 +106,6 @@ func TestGenerateCredentialsReturnsErrorOnRequestFailure(t *testing.T) { } dbCredentials, err := dbc.GenerateCredentials(ctx, dbRole) - require.Error(t, err, "Error does not exist") require.ErrorIs(t, err, resErr, "Error is incorrect") require.Empty(t, dbCredentials, "Credentials are not empty") } @@ -104,7 +121,15 @@ func TestGenerateCredentialsReturnsErrorOnInvalidResponseCode(t *testing.T) { tokenManager.On("GetToken").Return(token, nil) // Set up mocked API response with valid body but incorrect status code - resBody := "{\"data\": {\"username\": \"someusername\", \"password\": \"somepassword\"}}" + resBody := `{ + "lease_id": "someid", + "lease_duration": 300, + "renewable": true, + "data": { + "username": "someusername", + "password": "somepassword" + } + }` res := api.Response{ StatusCode: 418, RawBody: io.NopCloser(strings.NewReader(resBody)), @@ -212,11 +237,14 @@ func TestIntegrationGenerateCredentialsReturnsCredentials(t *testing.T) { dbCredentials, err := dbc.GenerateCredentials(ctx, dbRole) require.NoError(t, err, "Credential generation failure") require.NotEmpty(t, dbCredentials, "Credentials are empty") - require.NotEmpty(t, dbCredentials.Username, "Username is empty") - require.NotEqual(t, dbUser, dbCredentials.Username, "Username matches original credentials") - require.True(t, strings.HasPrefix(dbCredentials.Username, "v-token-"+dbRole)) - require.NotEmpty(t, dbCredentials.Password, "Password is empty") - require.NotEqual(t, dbPassword, dbCredentials.Password, "Password matches original credentials") + assert.NotEmpty(t, dbCredentials.Username, "Username is empty") + assert.NotEqual(t, dbUser, dbCredentials.Username, "Username matches original credentials") + assert.True(t, strings.HasPrefix(dbCredentials.Username, "v-token-"+dbRole)) + assert.NotEmpty(t, dbCredentials.Password, "Password is empty") + assert.NotEqual(t, dbPassword, dbCredentials.Password, "Password matches original credentials") + assert.NotEmpty(t, dbCredentials.Lease.ID, "Lease ID is empty") + assert.True(t, dbCredentials.Lease.Renewable, "Lease is not renewable") + assert.NotEmpty(t, dbCredentials.Lease.Expiration, "Lease expiration is empty") } func TestIntegrationGenerateCredentialsReturnsErrorOnInvalidDBEngineConfig(t *testing.T) {