mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +00:00 
			
		
		
		
	logical/postgresql: create DB credentials
This commit is contained in:
		| @@ -28,6 +28,11 @@ func Backend() *framework.Backend { | ||||
| 		Paths: []*framework.Path{ | ||||
| 			pathConfigConnection(&b), | ||||
| 			pathRoles(&b), | ||||
| 			pathRoleCreate(&b), | ||||
| 		}, | ||||
|  | ||||
| 		Secrets: []*framework.Secret{ | ||||
| 			secretCreds(&b), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| package postgresql | ||||
|  | ||||
| import ( | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	logicaltest "github.com/hashicorp/vault/logical/testing" | ||||
| 	"github.com/mitchellh/mapstructure" | ||||
| ) | ||||
|  | ||||
| func TestBackend_basic(t *testing.T) { | ||||
| @@ -15,6 +17,7 @@ func TestBackend_basic(t *testing.T) { | ||||
| 		Steps: []logicaltest.TestStep{ | ||||
| 			testAccStepConfig(t), | ||||
| 			testAccStepRole(t), | ||||
| 			testAccStepReadCreds(t, "web"), | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
| @@ -45,9 +48,27 @@ func testAccStepRole(t *testing.T) logicaltest.TestStep { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testAccStepReadCreds(t *testing.T, name string) logicaltest.TestStep { | ||||
| 	return logicaltest.TestStep{ | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Path:      name, | ||||
| 		Check: func(resp *logical.Response) error { | ||||
| 			var d struct { | ||||
| 				Username string `mapstructure:"username"` | ||||
| 				Password string `mapstructure:"password"` | ||||
| 			} | ||||
| 			if err := mapstructure.Decode(resp.Data, &d); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			log.Printf("[WARN] Generated credentials: %v", d) | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const testRole = ` | ||||
| CREATE ROLE {{name}} WITH | ||||
| CREATE ROLE "{{name}}" WITH | ||||
|   LOGIN | ||||
|   PASSWORD '{{password}}' | ||||
|   VALID UNTIL '{{expiration}}'; | ||||
|   PASSWORD '{{password}}'; | ||||
| ` | ||||
|   | ||||
							
								
								
									
										95
									
								
								builtin/logical/postgresql/path_role_create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								builtin/logical/postgresql/path_role_create.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| package postgresql | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| 	_ "github.com/lib/pq" | ||||
| ) | ||||
|  | ||||
| func pathRoleCreate(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: `(?P<name>\w+)`, | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"name": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Name of the role.", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.ReadOperation: b.pathRoleCreateRead, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathRoleCreateReadHelpSyn, | ||||
| 		HelpDescription: pathRoleCreateReadHelpDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleCreateRead( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	name := data.Get("name").(string) | ||||
|  | ||||
| 	// Get the role | ||||
| 	entry, err := req.Storage.Get("role/" + name) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil | ||||
| 	} | ||||
| 	var role struct { | ||||
| 		SQL string `json:"sql"` | ||||
| 	} | ||||
| 	if err := entry.DecodeJSON(&role); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Get our connection | ||||
| 	db, err := b.DB(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Generate our query | ||||
| 	username := fmt.Sprintf( | ||||
| 		"vault-%s-%d-%d", | ||||
| 		req.DisplayName, time.Now().Unix(), rand.Int31n(10000)) | ||||
| 	password := generateUUID() | ||||
| 	query := Query(role.SQL, map[string]string{ | ||||
| 		"name":       username, | ||||
| 		"password":   password, | ||||
| 		"expiration": "", | ||||
| 	}) | ||||
|  | ||||
| 	// Prepare the statement and execute it | ||||
| 	stmt, err := db.Prepare(query) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
| 	if _, err := stmt.Exec(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Return the secret | ||||
| 	return b.Secret(SecretCredsType).Response(map[string]interface{}{ | ||||
| 		"username": username, | ||||
| 		"password": password, | ||||
| 	}, map[string]interface{}{ | ||||
| 		"username": username, | ||||
| 	}), nil | ||||
| } | ||||
|  | ||||
| const pathRoleCreateReadHelpSyn = ` | ||||
| Request database credentials for a certain role. | ||||
| ` | ||||
|  | ||||
| const pathRoleCreateReadHelpDesc = ` | ||||
| This path reads database credentials for a certain role. The | ||||
| database credentials will be generated on demand and will be automatically | ||||
| revoked when the lease is up. | ||||
| ` | ||||
							
								
								
									
										59
									
								
								builtin/logical/postgresql/secret_creds.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								builtin/logical/postgresql/secret_creds.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| package postgresql | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| 	"github.com/lib/pq" | ||||
| ) | ||||
|  | ||||
| const SecretCredsType = "creds" | ||||
|  | ||||
| func secretCreds(b *backend) *framework.Secret { | ||||
| 	return &framework.Secret{ | ||||
| 		Type: SecretCredsType, | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"username": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Username", | ||||
| 			}, | ||||
|  | ||||
| 			"password": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Password", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Revoke: b.secretCredsRevoke, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *backend) secretCredsRevoke( | ||||
| 	req *logical.Request, d *framework.FieldData) (*logical.Response, error) { | ||||
| 	// Get the username from the internal data | ||||
| 	usernameRaw, ok := req.Secret.InternalData["username"] | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("secret is missing username internal data") | ||||
| 	} | ||||
| 	username, ok := usernameRaw.(string) | ||||
|  | ||||
| 	// Get our connection | ||||
| 	db, err := b.DB(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Drop this user | ||||
| 	stmt, err := db.Prepare(fmt.Sprintf( | ||||
| 		"DROP ROLE IF EXISTS %s;", pq.QuoteIdentifier(username))) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
| 	if _, err := stmt.Exec(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
							
								
								
									
										21
									
								
								builtin/logical/postgresql/uuid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								builtin/logical/postgresql/uuid.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package postgresql | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| // generateUUID is used to generate a random UUID | ||||
| func generateUUID() string { | ||||
| 	buf := make([]byte, 16) | ||||
| 	if _, err := rand.Read(buf); err != nil { | ||||
| 		panic(fmt.Errorf("failed to read random bytes: %v", err)) | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", | ||||
| 		buf[0:4], | ||||
| 		buf[4:6], | ||||
| 		buf[6:8], | ||||
| 		buf[8:10], | ||||
| 		buf[10:16]) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Mitchell Hashimoto
					Mitchell Hashimoto