aa6d61477e
VAULT-5827 Don't prepare SQL queries before executing them We don't support proper prepared statements, i.e., preparing once and executing many times since we do our own templating. So preparing our queries does not really accomplish anything, and can have severe performance impacts (see https://github.com/hashicorp/vault-plugin-database-snowflake/issues/13 for example). This behavior seems to have been copy-pasted for many years but not for any particular reason that we have been able to find. First use was in https://github.com/hashicorp/vault/pull/15 So here we switch to new methods suffixed with `Direct` to indicate that they don't `Prepare` before running `Exec`, and switch everything here to use those. We maintain the older methods with the existing behavior (with `Prepare`) for backwards compatibility.
181 lines
4.4 KiB
Go
181 lines
4.4 KiB
Go
package mssql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/helper/dbtxn"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
const SecretCredsType = "creds"
|
|
|
|
func secretCreds(b *backend) *framework.Secret {
|
|
return &framework.Secret{
|
|
Type: SecretCredsType,
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"username": {
|
|
Type: framework.TypeString,
|
|
Description: "Username",
|
|
},
|
|
|
|
"password": {
|
|
Type: framework.TypeString,
|
|
Description: "Password",
|
|
},
|
|
},
|
|
|
|
Renew: b.secretCredsRenew,
|
|
Revoke: b.secretCredsRevoke,
|
|
}
|
|
}
|
|
|
|
func (b *backend) secretCredsRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
// Get the lease information
|
|
leaseConfig, err := b.LeaseConfig(ctx, req.Storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if leaseConfig == nil {
|
|
leaseConfig = &configLease{}
|
|
}
|
|
|
|
resp := &logical.Response{Secret: req.Secret}
|
|
resp.Secret.TTL = leaseConfig.TTL
|
|
resp.Secret.MaxTTL = leaseConfig.TTLMax
|
|
return resp, nil
|
|
}
|
|
|
|
func (b *backend) secretCredsRevoke(ctx context.Context, 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(ctx, req.Storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// First disable server login
|
|
disableStmt, err := db.Prepare(fmt.Sprintf("ALTER LOGIN [%s] DISABLE;", username))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer disableStmt.Close()
|
|
if _, err := disableStmt.Exec(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Query for sessions for the login so that we can kill any outstanding
|
|
// sessions. There cannot be any active sessions before we drop the logins
|
|
// This isn't done in a transaction because even if we fail along the way,
|
|
// we want to remove as much access as possible
|
|
sessionStmt, err := db.Prepare("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = @p1;")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer sessionStmt.Close()
|
|
|
|
sessionRows, err := sessionStmt.Query(username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer sessionRows.Close()
|
|
|
|
var revokeStmts []string
|
|
for sessionRows.Next() {
|
|
var sessionID int
|
|
err = sessionRows.Scan(&sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
revokeStmts = append(revokeStmts, fmt.Sprintf("KILL %d;", sessionID))
|
|
}
|
|
|
|
// Query for database users using undocumented stored procedure for now since
|
|
// it is the easiest way to get this information;
|
|
// we need to drop the database users before we can drop the login and the role
|
|
// This isn't done in a transaction because even if we fail along the way,
|
|
// we want to remove as much access as possible
|
|
stmt, err := db.Prepare("EXEC master.dbo.sp_msloginmappings @p1;")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
rows, err := stmt.Query(username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var loginName, dbName, qUsername, aliasName sql.NullString
|
|
err = rows.Scan(&loginName, &dbName, &qUsername, &aliasName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !dbName.Valid {
|
|
continue
|
|
}
|
|
revokeStmts = append(revokeStmts, fmt.Sprintf(dropUserSQL, dbName.String, username, username))
|
|
}
|
|
|
|
// we do not stop on error, as we want to remove as
|
|
// many permissions as possible right now
|
|
var lastStmtError error
|
|
for _, query := range revokeStmts {
|
|
if err := dbtxn.ExecuteDBQueryDirect(ctx, db, nil, query); err != nil {
|
|
lastStmtError = err
|
|
continue
|
|
}
|
|
}
|
|
|
|
// can't drop if not all database users are dropped
|
|
if rows.Err() != nil {
|
|
return nil, fmt.Errorf("could not generate sql statements for all rows: %w", rows.Err())
|
|
}
|
|
if lastStmtError != nil {
|
|
return nil, fmt.Errorf("could not perform all sql statements: %w", lastStmtError)
|
|
}
|
|
|
|
// Drop this login
|
|
stmt, err = db.Prepare(fmt.Sprintf(dropLoginSQL, username, username))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer stmt.Close()
|
|
if _, err := stmt.Exec(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
const dropUserSQL = `
|
|
USE [%s]
|
|
IF EXISTS
|
|
(SELECT name
|
|
FROM sys.database_principals
|
|
WHERE name = N'%s')
|
|
BEGIN
|
|
DROP USER [%s]
|
|
END
|
|
`
|
|
|
|
const dropLoginSQL = `
|
|
IF EXISTS
|
|
(SELECT name
|
|
FROM master.sys.server_principals
|
|
WHERE name = N'%s')
|
|
BEGIN
|
|
DROP LOGIN [%s]
|
|
END
|
|
`
|