Skip to content

Commit abe1d79

Browse files
committed
feat: add support for impersonation
Fixes #417
1 parent 70d7cbf commit abe1d79

File tree

12 files changed

+398
-132
lines changed

12 files changed

+398
-132
lines changed

.envrc.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ export SQLSERVER_PASS="sqlserver-password"
1717
export SQLSERVER_DB="sqlserver-db-name"
1818

1919
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
20+
21+
# Requires the impersonating IAM principal to have
22+
# roles/iam.serviceAccountTokenCreator
23+
export IMPERSONATED_USER="[email protected]"

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ jobs:
108108
SQLSERVER_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
109109
SQLSERVER_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
110110
SQLSERVER_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_DB
111+
IMPERSONATED_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/IMPERSONATED_USER
111112
112113
- name: Enable fuse config (Linux)
113114
if: runner.os == 'Linux'
@@ -130,6 +131,7 @@ jobs:
130131
SQLSERVER_USER: '${{ steps.secrets.outputs.SQLSERVER_USER }}'
131132
SQLSERVER_PASS: '${{ steps.secrets.outputs.SQLSERVER_PASS }}'
132133
SQLSERVER_DB: '${{ steps.secrets.outputs.SQLSERVER_DB }}'
134+
IMPERSONATED_USER: '${{ steps.secrets.outputs.IMPERSONATED_USER }}'
133135
TMPDIR: "/tmp"
134136
TMP: '${{ runner.temp }}'
135137
# specifying bash shell ensures a failure in a piped process isn't lost by using `set -eo pipefail`

cmd/root.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ type Command struct {
8282
healthCheck bool
8383
httpAddress string
8484
httpPort string
85+
86+
// impersonationChain is a comma separated list of one or more service
87+
// accounts. The last entry in the chain is the impersonation target. Any
88+
// additional service accounts before the target are delegates. The
89+
// roles/iam.serviceAccountTokenCreator must be configured for each account
90+
// that will be impersonated.
91+
impersonationChain string
8592
}
8693

8794
// Option is a function that configures a Command.
@@ -253,6 +260,9 @@ https://cloud.google.com/storage/docs/requester-pays`)
253260
cmd.PersistentFlags().StringVar(&c.conf.FUSETempDir, "fuse-tmp-dir",
254261
filepath.Join(os.TempDir(), "csql-tmp"),
255262
"Temp dir for Unix sockets created with FUSE")
263+
cmd.PersistentFlags().StringVar(&c.impersonationChain, "impersonate-service-account", "",
264+
`Comma separated list of service accounts to impersonate. Last value
265+
is the target account.`)
256266

257267
// Global and per instance flags
258268
cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
@@ -338,7 +348,10 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
338348
if userHasSet("sqladmin-api-endpoint") && conf.APIEndpointURL != "" {
339349
_, err := url.Parse(conf.APIEndpointURL)
340350
if err != nil {
341-
return newBadCommandError(fmt.Sprintf("the value provided for --sqladmin-api-endpoint is not a valid URL, %v", conf.APIEndpointURL))
351+
return newBadCommandError(fmt.Sprintf(
352+
"the value provided for --sqladmin-api-endpoint is not a valid URL, %v",
353+
conf.APIEndpointURL,
354+
))
342355
}
343356

344357
// add a trailing '/' if omitted
@@ -347,6 +360,16 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
347360
}
348361
}
349362

363+
if cmd.impersonationChain != "" {
364+
accts := strings.Split(cmd.impersonationChain, ",")
365+
conf.ImpersonateTarget = accts[0]
366+
// Assign delegates if the chain is more than one account.
367+
if l := len(accts); l > 1 {
368+
conf.ImpersonateTarget = accts[l-1]
369+
conf.ImpersonateDelegates = accts[:l-1]
370+
}
371+
}
372+
350373
var ics []proxy.InstanceConnConfig
351374
for _, a := range args {
352375
// Assume no query params initially

cmd/root_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,19 @@ func TestNewCommandArguments(t *testing.T) {
279279
QuotaProject: "proj",
280280
}),
281281
},
282+
{
283+
desc: "",
284+
args: []string{"--impersonate-service-account",
285+
286+
"proj:region:inst"},
287+
want: withDefaults(&proxy.Config{
288+
ImpersonateTarget: "[email protected]",
289+
ImpersonateDelegates: []string{
290+
291+
292+
},
293+
}),
294+
},
282295
}
283296

284297
for _, tc := range tcs {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ require (
66
cloud.google.com/go/cloudsqlconn v0.5.2
77
contrib.go.opencensus.io/exporter/prometheus v0.4.2
88
contrib.go.opencensus.io/exporter/stackdriver v0.13.14
9-
github.com/denisenkom/go-mssqldb v0.12.2
109
github.com/go-sql-driver/mysql v1.6.0
1110
github.com/google/go-cmp v0.5.9
1211
github.com/hanwen/go-fuse/v2 v2.1.0
1312
github.com/jackc/pgx/v4 v4.17.2
13+
github.com/microsoft/go-mssqldb v0.17.0
1414
github.com/spf13/cobra v1.5.0
1515
go.opencensus.io v0.23.0
1616
go.uber.org/zap v1.23.0

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,11 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h
7474
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
7575
github.com/Azure/azure-sdk-for-go v63.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
7676
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
77+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
7778
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
79+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM=
7880
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
81+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
7982
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
8083
github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
8184
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
@@ -97,6 +100,7 @@ github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYX
97100
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
98101
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
99102
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
103+
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
100104
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
101105
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
102106
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@@ -355,6 +359,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
355359
github.com/dgryski/go-sip13 v0.0.0-20200911182023-62edffca9245/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
356360
github.com/digitalocean/godo v1.78.0/go.mod h1:GBmu8MkjZmNARE7IXRPmkbbnocNN8+uBm0xbEVw2LCs=
357361
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
362+
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
358363
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
359364
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
360365
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
@@ -516,6 +521,8 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5
516521
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
517522
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
518523
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
524+
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
525+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
519526
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
520527
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
521528
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
@@ -847,6 +854,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
847854
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
848855
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
849856
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
857+
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
858+
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
850859
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
851860
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
852861
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -885,6 +894,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
885894
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
886895
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
887896
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
897+
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
888898
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
889899
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
890900
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@@ -957,6 +967,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
957967
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
958968
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
959969
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
970+
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
971+
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
960972
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
961973
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
962974
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1262,6 +1274,7 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
12621274
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
12631275
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
12641276
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
1277+
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
12651278
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
12661279
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
12671280
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1343,6 +1356,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
13431356
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
13441357
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
13451358
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
1359+
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
13461360
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
13471361
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
13481362
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -1514,6 +1528,7 @@ golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBc
15141528
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15151529
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15161530
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1531+
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15171532
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15181533
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15191534
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1536,6 +1551,7 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
15361551
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15371552
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15381553
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1554+
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15391555
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15401556
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15411557
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/proxy/proxy.go

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import (
3030
"github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cloudsql"
3131
"github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/gcloud"
3232
"golang.org/x/oauth2"
33+
"google.golang.org/api/impersonate"
34+
"google.golang.org/api/option"
35+
"google.golang.org/api/sqladmin/v1"
3336
)
3437

3538
var (
@@ -160,6 +163,15 @@ type Config struct {
160163
// API request quotas.
161164
QuotaProject string
162165

166+
// ImpersonateTarget is the service account to impersonate. The IAM
167+
// principal doing the impersonation must have the
168+
// roles/iam.serviceAccountTokenCreator role.
169+
ImpersonateTarget string
170+
// ImpersonateDelegates are the intermediate service accounts through which
171+
// the impersonation is achieved. Each delegate must have the
172+
// roles/iam.serviceAccountTokenCreator role.
173+
ImpersonateDelegates []string
174+
163175
// StructuredLogs sets all output to use JSON in the LogEntry format.
164176
// See https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
165177
StructuredLogs bool
@@ -187,38 +199,89 @@ func (c *Config) DialOptions(i InstanceConnConfig) []cloudsqlconn.DialOption {
187199
return opts
188200
}
189201

190-
// DialerOptions builds appropriate list of options from the Config
191-
// values for use by cloudsqlconn.NewClient()
192-
func (c *Config) DialerOptions(l cloudsql.Logger) ([]cloudsqlconn.Option, error) {
193-
opts := []cloudsqlconn.Option{
194-
cloudsqlconn.WithUserAgent(c.UserAgent),
202+
func (c *Config) credentialsOpt(l cloudsql.Logger) (cloudsqlconn.Option, error) {
203+
// If service account impersonation is configured, set up an impersonated
204+
// credentials token source.
205+
if c.ImpersonateTarget != "" {
206+
var iopts []option.ClientOption
207+
switch {
208+
case c.Token != "":
209+
l.Infof("Impersonating service account with OAuth2 token")
210+
iopts = append(iopts, option.WithTokenSource(
211+
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}),
212+
))
213+
case c.CredentialsFile != "":
214+
l.Infof("Impersonating service account with the credentials file at %q", c.CredentialsFile)
215+
iopts = append(iopts, option.WithCredentialsFile(c.CredentialsFile))
216+
case c.CredentialsJSON != "":
217+
l.Infof("Impersonating service account with JSON credentials environment variable")
218+
iopts = append(iopts, option.WithCredentialsJSON([]byte(c.CredentialsJSON)))
219+
case c.GcloudAuth:
220+
l.Infof("Impersonating service account with gcloud user credentials")
221+
ts, err := gcloud.TokenSource()
222+
if err != nil {
223+
return nil, err
224+
}
225+
iopts = append(iopts, option.WithTokenSource(ts))
226+
default:
227+
l.Infof("Impersonating service account with Application Default Credentials")
228+
}
229+
ts, err := impersonate.CredentialsTokenSource(
230+
context.Background(),
231+
impersonate.CredentialsConfig{
232+
TargetPrincipal: c.ImpersonateTarget,
233+
Delegates: c.ImpersonateDelegates,
234+
Scopes: []string{
235+
sqladmin.CloudPlatformScope,
236+
sqladmin.SqlserviceAdminScope,
237+
},
238+
},
239+
iopts...,
240+
)
241+
if err != nil {
242+
return nil, err
243+
}
244+
return cloudsqlconn.WithTokenSource(ts), nil
195245
}
246+
247+
// Otherwise, configure credentials as usual.
196248
switch {
197249
case c.Token != "":
198-
l.Infof("Authorizing with the -token flag")
199-
opts = append(opts, cloudsqlconn.WithTokenSource(
250+
l.Infof("Authorizing with OAuth2 token")
251+
return cloudsqlconn.WithTokenSource(
200252
oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}),
201-
))
253+
), nil
202254
case c.CredentialsFile != "":
203255
l.Infof("Authorizing with the credentials file at %q", c.CredentialsFile)
204-
opts = append(opts, cloudsqlconn.WithCredentialsFile(
205-
c.CredentialsFile,
206-
))
256+
return cloudsqlconn.WithCredentialsFile(c.CredentialsFile), nil
207257
case c.CredentialsJSON != "":
208258
l.Infof("Authorizing with JSON credentials environment variable")
209-
opts = append(opts, cloudsqlconn.WithCredentialsJSON(
210-
[]byte(c.CredentialsJSON),
211-
))
259+
return cloudsqlconn.WithCredentialsJSON([]byte(c.CredentialsJSON)), nil
212260
case c.GcloudAuth:
213261
l.Infof("Authorizing with gcloud user credentials")
214262
ts, err := gcloud.TokenSource()
215263
if err != nil {
216264
return nil, err
217265
}
218-
opts = append(opts, cloudsqlconn.WithTokenSource(ts))
266+
return cloudsqlconn.WithTokenSource(ts), nil
219267
default:
220268
l.Infof("Authorizing with Application Default Credentials")
269+
// Return no-op options to avoid having to handle nil in caller code
270+
return cloudsqlconn.WithOptions(), nil
271+
}
272+
}
273+
274+
// DialerOptions builds appropriate list of options from the Config
275+
// values for use by cloudsqlconn.NewClient()
276+
func (c *Config) DialerOptions(l cloudsql.Logger) ([]cloudsqlconn.Option, error) {
277+
opts := []cloudsqlconn.Option{
278+
cloudsqlconn.WithUserAgent(c.UserAgent),
279+
}
280+
co, err := c.credentialsOpt(l)
281+
if err != nil {
282+
return nil, err
221283
}
284+
opts = append(opts, co)
222285

223286
if c.APIEndpointURL != "" {
224287
opts = append(opts, cloudsqlconn.WithAdminAPIEndpoint(c.APIEndpointURL))

tests/common_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"bytes"
2626
"context"
2727
"errors"
28+
"flag"
2829
"fmt"
2930
"io"
3031
"os"
@@ -34,6 +35,14 @@ import (
3435
"github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/log"
3536
)
3637

38+
var (
39+
impersonatedUser = flag.String(
40+
"impersonated_user",
41+
os.Getenv("IMPERSONATED_USER"),
42+
"Name of the service account that supports impersonation (impersonator must have roles/iam.serviceAccountTokenCreator)",
43+
)
44+
)
45+
3746
// ProxyExec represents an execution of the Cloud SQL proxy.
3847
type ProxyExec struct {
3948
Out io.ReadCloser

tests/connection_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ const connTestTimeout = time.Minute
3535
// and then unsets GOOGLE_APPLICATION_CREDENTIALS. It returns a cleanup function
3636
// that restores the original setup.
3737
func removeAuthEnvVar(t *testing.T) (*oauth2.Token, string, func()) {
38-
ts, err := google.DefaultTokenSource(context.Background(), sqladmin.SqlserviceAdminScope)
38+
ts, err := google.DefaultTokenSource(context.Background(),
39+
sqladmin.CloudPlatformScope,
40+
sqladmin.SqlserviceAdminScope,
41+
)
3942
if err != nil {
4043
t.Errorf("failed to resolve token source: %v", err)
4144
}

0 commit comments

Comments
 (0)