Skip to content

Commit c9c5e05

Browse files
Add passwdCmd config option
It iso now possible to define a command that is executed in order to optain the password. This is useful in situations, where your database is manged by a cloud provider and you are using the cloud providers IAM solution to gain the password. Fixes sqls-server#154
1 parent eb695ac commit c9c5e05

10 files changed

+157
-20
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ connections:
111111
params:
112112
autocommit: "true"
113113
tls: skip-verify
114+
- alias: managed_mysql
115+
driver: mysql
116+
proto: tcp
117+
user: root
118+
passwdCmd: ["echo", "-n", "super_secure"]
119+
host: 127.0.0.1
120+
port: 13306
121+
dbName: world
114122
- alias: mysql_via_ssh
115123
driver: mysql
116124
proto: tcp
@@ -232,7 +240,9 @@ The first setting in `connections` is the default connection.
232240

233241
### connections
234242

235-
`dataSourceName` takes precedence over the value set in `proto`, `user`, `passwd`, `host`, `port`, `dbName`, `params`.
243+
`dataSourceName` takes precedence over the value set in `proto`, `user`, `passwd`, `passwdCmd`, `host`, `port`, `dbName`, `params`.
244+
245+
`passwdCmd` takes precedence over the value set in `passwd`.
236246

237247
| Key | Description |
238248
| -------------- | ------------------------------------------- |
@@ -242,6 +252,7 @@ The first setting in `connections` is the default connection.
242252
| proto | `tcp`, `udp`, `unix`. |
243253
| user | User name |
244254
| passwd | Password |
255+
| passwdCmd | Command to be executed to get password (Array) |
245256
| host | Host |
246257
| port | Port |
247258
| path | unix socket path |

internal/config/config_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ func TestGetConfig(t *testing.T) {
3939
DBName: "world",
4040
Params: map[string]string{"autocommit": "true", "tls": "skip-verify"},
4141
},
42+
{
43+
Alias: "sqls_mysql",
44+
Driver: "mysql",
45+
Proto: "tcp",
46+
User: "root",
47+
PasswdCmd: []string{"echo", "topsecret"},
48+
Host: "127.0.0.1",
49+
Port: 13306,
50+
DBName: "world",
51+
Params: map[string]string{"autocommit": "true", "tls": "skip-verify"},
52+
},
4253
{
4354
Alias: "sqls_sqlite3",
4455
Driver: "sqlite3",

internal/database/clickhouse.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,15 @@ func genClickhouseDsn(dbConfig *DBConfig) (string, error) {
101101
return "", fmt.Errorf("unsupported protocol %s", dbConfig.Proto)
102102
}
103103

104-
if dbConfig.Passwd == "" {
104+
passwd, err := dbConfig.ResolvePassword()
105+
if err != nil {
106+
return "", err
107+
}
108+
109+
if passwd == "" {
105110
u.User = url.User(dbConfig.User)
106111
} else {
107-
u.User = url.UserPassword(dbConfig.User, dbConfig.Passwd)
112+
u.User = url.UserPassword(dbConfig.User, passwd)
108113
}
109114

110115
u.Host = fmt.Sprintf("%s:%d", dbConfig.Host, dbConfig.Port)
@@ -131,8 +136,13 @@ func genClickhouseConfig(dbConfig *DBConfig) (*clickhouse.Options, error) {
131136

132137
cfg := &clickhouse.Options{}
133138

139+
passwd, err := dbConfig.ResolvePassword()
140+
if err != nil {
141+
return nil, err
142+
}
143+
134144
cfg.Auth.Username = dbConfig.User
135-
cfg.Auth.Password = dbConfig.Passwd
145+
cfg.Auth.Password = passwd
136146
cfg.Auth.Database = dbConfig.DBName
137147

138148
switch dbConfig.Proto {

internal/database/config.go

+19-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"os"
7+
"os/exec"
78

89
"github.com/sqls-server/sqls/dialect"
910
"golang.org/x/crypto/ssh"
@@ -25,6 +26,7 @@ type DBConfig struct {
2526
Proto Proto `json:"proto" yaml:"proto"`
2627
User string `json:"user" yaml:"user"`
2728
Passwd string `json:"passwd" yaml:"passwd"`
29+
PasswdCmd []string `json:"passwdCmd" yaml:"passwdCmd"`
2830
Host string `json:"host" yaml:"host"`
2931
Port int `json:"port" yaml:"port"`
3032
Path string `json:"path" yaml:"path"`
@@ -33,6 +35,20 @@ type DBConfig struct {
3335
SSHCfg *SSHConfig `json:"sshConfig" yaml:"sshConfig"`
3436
}
3537

38+
func (c *DBConfig) ResolvePassword() (string, error) {
39+
if len(c.PasswdCmd) == 0 {
40+
return c.Passwd, nil
41+
}
42+
43+
cmd := exec.Command(c.PasswdCmd[0], c.PasswdCmd[1:]...)
44+
data, err := cmd.Output()
45+
if err != nil {
46+
return "", err
47+
}
48+
49+
return string(data), nil
50+
}
51+
3652
func (c *DBConfig) Validate() error {
3753
if c.Driver == "" {
3854
return errors.New("required: connections[].driver")
@@ -101,8 +117,8 @@ func (c *DBConfig) Validate() error {
101117
if c.User == "" {
102118
return errors.New("required: connections[].user")
103119
}
104-
if c.Passwd == "" {
105-
return errors.New("required: connections[].Passwd")
120+
if len(c.PasswdCmd) == 0 && c.Passwd == "" {
121+
return errors.New("required: connections[].PasswdCmd or connections[].Passwd")
106122
}
107123
if c.Host == "" {
108124
return errors.New("required: connections[].Host")
@@ -129,7 +145,7 @@ func (c *DBConfig) Validate() error {
129145
return errors.New("required: connections[].host")
130146
}
131147
case ProtoUDP, ProtoUnix:
132-
default:
148+
default:
133149
return errors.New("invalid: connections[].proto")
134150
}
135151
if c.SSHCfg != nil {

internal/database/config_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package database
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
)
8+
9+
func TestResolvePassword(t *testing.T) {
10+
type testCase struct {
11+
title string
12+
dbConfig DBConfig
13+
want string
14+
wantErr bool
15+
}
16+
17+
testCases := []testCase{
18+
{
19+
title: "only password",
20+
dbConfig: DBConfig{
21+
Passwd: "test",
22+
},
23+
want: "test",
24+
},
25+
{
26+
title: "only command",
27+
dbConfig: DBConfig{
28+
PasswdCmd: []string{"echo", "-n", "secure"},
29+
},
30+
want: "secure",
31+
},
32+
{
33+
title: "password and command",
34+
dbConfig: DBConfig{
35+
Passwd: "test",
36+
PasswdCmd: []string{"echo", "-n", "secure"},
37+
},
38+
want: "secure",
39+
},
40+
{
41+
title: "failing command",
42+
dbConfig: DBConfig{
43+
Passwd: "test",
44+
PasswdCmd: []string{"false"},
45+
},
46+
wantErr: true,
47+
},
48+
}
49+
50+
for _, tt := range testCases {
51+
t.Run(tt.title, func(t *testing.T) {
52+
got, err := tt.dbConfig.ResolvePassword()
53+
if err != nil {
54+
if !tt.wantErr {
55+
t.Errorf("ResolvePassword() error = %v, wantErr %v", err, tt.wantErr)
56+
return
57+
}
58+
}
59+
if diff := cmp.Diff(tt.want, got); diff != "" {
60+
t.Errorf("unmatch (- want, + got):\n%s", diff)
61+
}
62+
})
63+
}
64+
}

internal/database/mssql.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package database
22

33
import (
4-
"os"
54
"context"
65
"database/sql"
76
"fmt"
87
"log"
98
"net/url"
9+
"os"
1010
"strconv"
1111

1212
_ "github.com/denisenkom/go-mssqldb"
13-
"github.com/sqls-server/sqls/dialect"
1413
"github.com/jfcote87/sshdb"
1514
"github.com/jfcote87/sshdb/mssql"
15+
"github.com/sqls-server/sqls/dialect"
1616
"golang.org/x/crypto/ssh"
1717
)
1818

@@ -23,7 +23,7 @@ func init() {
2323

2424
func mssqlOpen(dbConnCfg *DBConfig) (*DBConnection, error) {
2525
var (
26-
conn *sql.DB
26+
conn *sql.DB
2727
)
2828
dsn, err := genMssqlConfig(dbConnCfg)
2929
if err != nil {
@@ -41,9 +41,9 @@ func mssqlOpen(dbConnCfg *DBConfig) (*DBConnection, error) {
4141
return nil, fmt.Errorf("unable to decrypt private key")
4242
}
4343

44-
cfg := &ssh.ClientConfig {
44+
cfg := &ssh.ClientConfig{
4545
User: dbConnCfg.SSHCfg.User,
46-
Auth: []ssh.AuthMethod {
46+
Auth: []ssh.AuthMethod{
4747
ssh.PublicKeys(signer),
4848
},
4949
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
@@ -77,7 +77,7 @@ func mssqlOpen(dbConnCfg *DBConfig) (*DBConnection, error) {
7777
conn.SetMaxOpenConns(DefaultMaxOpenConns)
7878

7979
return &DBConnection{
80-
Conn: conn,
80+
Conn: conn,
8181
}, nil
8282
}
8383

@@ -376,9 +376,14 @@ func genMssqlConfig(connCfg *DBConfig) (string, error) {
376376
return connCfg.DataSourceName, nil
377377
}
378378

379+
passwd, err := connCfg.ResolvePassword()
380+
if err != nil {
381+
return "", err
382+
}
383+
379384
q := url.Values{}
380385
q.Set("user", connCfg.User)
381-
q.Set("password", connCfg.Passwd)
386+
q.Set("password", passwd)
382387
q.Set("database", connCfg.DBName)
383388

384389
switch connCfg.Proto {

internal/database/mysql.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,13 @@ func genMysqlConfig(connCfg *DBConfig) (*mysql.Config, error) {
9494
return mysql.ParseDSN(connCfg.DataSourceName)
9595
}
9696

97+
passwd, err := connCfg.ResolvePassword()
98+
if err != nil {
99+
return nil, err
100+
}
101+
97102
cfg.User = connCfg.User
98-
cfg.Passwd = connCfg.Passwd
103+
cfg.Passwd = passwd
99104
cfg.DBName = connCfg.DBName
100105

101106
switch connCfg.Proto {
@@ -116,7 +121,7 @@ func genMysqlConfig(connCfg *DBConfig) (*mysql.Config, error) {
116121
}
117122
cfg.Addr = connCfg.Path
118123
cfg.Net = string(connCfg.Proto)
119-
case ProtoHTTP:
124+
case ProtoHTTP:
120125
default:
121126
return nil, fmt.Errorf("default addr for network %s unknown", connCfg.Proto)
122127
}

internal/database/oracle.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ func genOracleConfig(connCfg *DBConfig) (string, error) {
5050
if port == 0 {
5151
port = 1521
5252
}
53-
DSName := connCfg.User + "/" + connCfg.Passwd + "@" + host + ":" + strconv.Itoa(port) + "/" + connCfg.DBName
53+
passwd, err := connCfg.ResolvePassword()
54+
if err != nil {
55+
return "", err
56+
}
57+
58+
DSName := connCfg.User + "/" + passwd + "@" + host + ":" + strconv.Itoa(port) + "/" + connCfg.DBName
5459
return DSName, nil
5560
}
5661

@@ -106,7 +111,7 @@ func (db *OracleDBRepository) SchemaTables(ctx context.Context) (map[string][]st
106111
ctx,
107112
`
108113
SELECT OWNER, TABLE_NAME
109-
FROM SYS.ALL_TABLES
114+
FROM SYS.ALL_TABLES
110115
ORDER BY OWNER, TABLE_NAME
111116
`)
112117
if err != nil {

internal/database/postgresql.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,14 @@ func genPostgresConfig(connCfg *DBConfig) (string, error) {
396396
return connCfg.DataSourceName, nil
397397
}
398398

399+
passwd, err := connCfg.ResolvePassword()
400+
if err != nil {
401+
return "", err
402+
}
403+
399404
q := url.Values{}
400405
q.Set("user", connCfg.User)
401-
q.Set("password", connCfg.Passwd)
406+
q.Set("password", passwd)
402407
q.Set("dbname", connCfg.DBName)
403408

404409
switch connCfg.Proto {

internal/database/vertica.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ func genVerticaConfig(connCfg *DBConfig) (string, error) {
5151
port = 5433
5252
}
5353

54-
DSName := connCfg.User + "/" + connCfg.Passwd + "@" + host + ":" + strconv.Itoa(port) + "/" + connCfg.DBName
54+
passwd, err := connCfg.ResolvePassword()
55+
if err != nil {
56+
return "", err
57+
}
58+
59+
DSName := connCfg.User + "/" + passwd + "@" + host + ":" + strconv.Itoa(port) + "/" + connCfg.DBName
5560
return DSName, nil
5661
}
5762

0 commit comments

Comments
 (0)