diff --git a/.gitignore b/.gitignore index 3867aa70..03ea8415 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ gon.granted.arm.hcl launch.json .env -.vscode/ \ No newline at end of file +.vscode/ + +bad-token.json \ No newline at end of file diff --git a/go.mod b/go.mod index 68980976..aa163415 100644 --- a/go.mod +++ b/go.mod @@ -33,13 +33,17 @@ require ( github.com/common-fate/sdk v1.69.0 github.com/common-fate/xid v1.0.0 github.com/fatih/color v1.16.0 + github.com/google/go-cmp v0.6.0 github.com/hashicorp/yamux v0.1.1 + github.com/imdario/mergo v0.3.11 github.com/lithammer/fuzzysearch v1.1.5 github.com/mattn/go-runewidth v0.0.16 github.com/schollz/progressbar/v3 v3.13.1 go.uber.org/zap v1.26.0 - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 + k8s.io/client-go v0.28.4 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -59,26 +63,29 @@ require ( github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/common-fate/iso8601 v1.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deepmap/oapi-codegen v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getkin/kin-openapi v0.107.0 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/huandu/xstrings v1.3.3 // indirect - github.com/imdario/mergo v0.3.11 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -86,18 +93,22 @@ require ( github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/muhlemmer/gu v0.3.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xtaci/smux v1.5.24 // indirect github.com/zitadel/logging v0.6.0 // indirect @@ -108,8 +119,16 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/time v0.3.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) require ( @@ -148,6 +167,7 @@ require ( golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.18.0 gopkg.in/ini.v1 v1.67.0 + k8s.io/apimachinery v0.31.1 ) replace github.com/aws/session-manager-plugin => github.com/common-fate/session-manager-plugin v0.0.0-20240723053832-3d311db99016 diff --git a/go.sum b/go.sum index f7174131..f0717aa4 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,9 @@ github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= @@ -124,6 +125,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -132,6 +135,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= github.com/getkin/kin-openapi v0.107.0 h1:bxhL6QArW7BXQj8NjXfIJQy680NsMKd25nwhvpCXchg= github.com/getkin/kin-openapi v0.107.0/go.mod h1:9Dhr+FasATJZjS4iOLvB0hkaxgYdulrNYm2e9epLWOo= @@ -151,10 +156,13 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= @@ -165,12 +173,19 @@ github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -211,18 +226,26 @@ github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-client/go v0.0.0-20211217162629-92040c8d5731 h1:M5SwNEt6dLz/Tkvc5KTM/Ma7iAPuKgA3uhARBhb9b40= +github.com/kubernetes-client/go v0.0.0-20211217162629-92040c8d5731/go.mod h1:ks4KCmmxdXksTSu2dlnUanEOqNd/dsoyS6/7bay2RQ8= +github.com/kubernetes-client/go-base v0.0.0-20190205182333-3d0e39759d98 h1:ZMIkOkl/Bg5H4EJI7zbjVXAo4rV0QJOGz2U5A0xUmZU= +github.com/kubernetes-client/go-base v0.0.0-20190205182333-3d0e39759d98/go.mod h1:HPlr4uJEfrxar3JUY9cmXs3oooPjTLO6nEaEAIt5LI8= github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= @@ -275,8 +298,10 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= @@ -292,6 +317,8 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= @@ -300,14 +327,16 @@ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -324,6 +353,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -350,10 +381,14 @@ github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY= github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= @@ -379,6 +414,7 @@ go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyB go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -388,10 +424,14 @@ golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -403,6 +443,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -411,6 +453,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -448,8 +491,12 @@ golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -460,8 +507,8 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -469,6 +516,8 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= @@ -484,3 +533,25 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/cfcfg/cfcfg.go b/pkg/cfcfg/cfcfg.go index 7347bc10..f70f6312 100644 --- a/pkg/cfcfg/cfcfg.go +++ b/pkg/cfcfg/cfcfg.go @@ -36,6 +36,15 @@ func GetCommonFateURL(profile *cfaws.Profile) (*url.URL, error) { return u, nil } +func GenerateRequestURL(apiURL string, requestID string) (string, error) { + u, err := url.Parse(apiURL) + if err != nil { + return "", err + } + p := u.JoinPath("access", "requests", requestID) + return p.String(), nil +} + func Load(ctx context.Context, profile *cfaws.Profile) (*sdkconfig.Context, error) { cfURL, err := GetCommonFateURL(profile) if err != nil { diff --git a/pkg/granted/eks/config.go b/pkg/granted/eks/config.go new file mode 100644 index 00000000..f8d726be --- /dev/null +++ b/pkg/granted/eks/config.go @@ -0,0 +1,93 @@ +package eks + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/common-fate/clio" + "github.com/common-fate/granted/pkg/granted/proxy" + accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" + "github.com/fatih/color" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" +) + +func OpenKubeConfig() (*api.Config, string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, "", err + } + + kubeConfigPath := filepath.Join(homeDir, ".kube", "config") + + loader := clientcmd.ClientConfigLoadingRules{ + Precedence: []string{kubeConfigPath}, + WarnIfAllMissing: true, + Warner: func(err error) { + // debug log the warning if teh file does not exist + // it will default to creating a new file + clio.Debug(err) + }, + } + config, err := loader.Load() + if err != nil { + return nil, "", err + } + + return config, kubeConfigPath, nil +} + +func AddContextToConfig(ensureAccessOutput *proxy.EnsureAccessOutput[*accessv1alpha1.AWSEKSProxyOutput], port string) error { + + kc, kubeConfigPath, err := OpenKubeConfig() + if err != nil { + return err + } + + clusterContextName := fmt.Sprintf("cf-grant-to-%s-as-%s", ensureAccessOutput.GrantOutput.EksCluster.Name, ensureAccessOutput.GrantOutput.ServiceAccountName) + // Use the same name for the context and the cluster, so that each grant is assigned a unique entry for the cluster + clusterName := clusterContextName + + username := ensureAccessOutput.GrantOutput.ServiceAccountName + + // remove an existing value for the context being added/updated + delete(kc.Contexts, clusterContextName) + // remove existing cluster definitions so they can be reset + delete(kc.Clusters, clusterName) + // remove existing user definitions so they can be reset + delete(kc.AuthInfos, username) + + newCluster := api.NewCluster() + newCluster.Server = fmt.Sprintf("http://localhost:%s", port) + newCluster.InsecureSkipTLSVerify = true + //add the new cluster and context back in + kc.Clusters[clusterName] = newCluster + + newContext := api.NewContext() + newContext.Cluster = clusterName + newContext.AuthInfo = username + // @TODO, teams may wish to specify a default namespace for each user or cluster? + newContext.Namespace = "default" + kc.Contexts[clusterContextName] = newContext + + newUser := api.NewAuthInfo() + newUser.Impersonate = username + kc.AuthInfos[username] = newUser + + err = clientcmd.WriteToFile(*kc, kubeConfigPath) + if err != nil { + return err + } + + //set the context + clio.Infof("EKS proxy is ready for connections") + clio.Infof("Your `~/.kube/config` file has been updated with a new cluster context. To connect to this cluster, run the following command to switch your current context:") + clio.Log(color.YellowString("kubectl config use-context %s", clusterContextName)) + clio.NewLine() + clio.Infof("Or using the --context flag with kubectl: %s", color.YellowString("kubectl --context=%s", clusterContextName)) + clio.NewLine() + + return nil + +} diff --git a/pkg/granted/eks/eks.go b/pkg/granted/eks/eks.go new file mode 100644 index 00000000..a9623198 --- /dev/null +++ b/pkg/granted/eks/eks.go @@ -0,0 +1,215 @@ +package eks + +import ( + "context" + "errors" + + "connectrpc.com/connect" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/common-fate/clio" + "github.com/common-fate/grab" + "github.com/common-fate/granted/pkg/cfcfg" + "github.com/common-fate/granted/pkg/granted/proxy" + "github.com/common-fate/sdk/config" + accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" + "github.com/common-fate/sdk/service/access" + "github.com/mattn/go-runewidth" + + "github.com/urfave/cli/v2" +) + +var Command = cli.Command{ + Name: "eks", + Usage: "Granted EKS plugin", + Description: "Granted EKS plugin", + Subcommands: []*cli.Command{&proxyCommand}, +} + +// isLocalMode is used where some behaviour needs to be changed to run against a local development proxy server +func isLocalMode(c *cli.Context) bool { + return c.String("mode") == "local" +} + +var proxyCommand = cli.Command{ + Name: "proxy", + Usage: "The Proxy plugin is used in conjunction with a Commnon Fate deployment to request temporary access to an AWS EKS Cluster", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "target", Aliases: []string{"cluster"}}, + &cli.StringFlag{Name: "role", Aliases: []string{"service-account"}}, + &cli.StringFlag{Name: "reason", Usage: "Provide a reason for requesting access to the role"}, + &cli.BoolFlag{Name: "confirm", Aliases: []string{"y"}, Usage: "Skip confirmation prompts for access requests"}, + &cli.BoolFlag{Name: "wait", Value: true, Usage: "Wait for the access request to be approved."}, + &cli.BoolFlag{Name: "no-cache", Usage: "Disables caching of session credentials and forces a refresh", EnvVars: []string{"GRANTED_NO_CACHE"}}, + &cli.DurationFlag{Name: "duration", Aliases: []string{"d"}, Usage: "The duration for your access request"}, + &cli.StringFlag{Name: "mode", Hidden: true, Usage: "What mode to run the proxy command in, [remote,local], local is used in development to connect to a local instance of the proxy server rather than remote via SSM", Value: "remote"}, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + cfg, err := config.LoadDefault(ctx) + if err != nil { + return err + } + + err = cfg.Initialize(ctx, config.InitializeOpts{}) + if err != nil { + return err + } + + ensuredAccess, err := proxy.EnsureAccess(ctx, cfg, proxy.EnsureAccessInput[*accessv1alpha1.AWSEKSProxyOutput]{ + Target: c.String("target"), + Role: c.String("role"), + Duration: c.Duration("duration"), + Reason: c.String("reason"), + Confirm: c.Bool("confirm"), + Wait: c.Bool("wait"), + PromptForEntitlement: promptForClusterAndRole, + GetGrantOutput: func(msg *accessv1alpha1.GetGrantOutputResponse) (*accessv1alpha1.AWSEKSProxyOutput, error) { + output := msg.GetOutputAwsEksProxy() + if output == nil { + return nil, errors.New("unexpected grant output, this indicates an error in the Common Fate Provisioning process, you should contect your Common Fate administrator") + } + return output, nil + }, + }) + if err != nil { + return err + } + + requestURL, err := cfcfg.GenerateRequestURL(cfg.APIURL, ensuredAccess.Grant.AccessRequestId) + if err != nil { + return err + } + + serverPort, localPort, err := proxy.Ports(isLocalMode(c)) + if err != nil { + return err + } + + clio.Debugw("prepared ports for access", "serverPort", serverPort, "localPort", localPort) + // In local mode ssm is not used, instead, the command connects directly to the proxy service running in local dev + // Return early because there is nothing to startup + if !isLocalMode(c) { + err = proxy.WaitForSSMConnectionToProxyServer(ctx, proxy.WaitForSSMConnectionToProxyServerOpts{ + AWSConfig: proxy.AWSConfig{ + SSOAccountID: ensuredAccess.GrantOutput.EksCluster.AccountId, + SSORoleName: ensuredAccess.Grant.Id, + SSORegion: ensuredAccess.GrantOutput.SsoRegion, + SSOStartURL: ensuredAccess.GrantOutput.SsoStartUrl, + Region: ensuredAccess.GrantOutput.EksCluster.Region, + SSMSessionTarget: ensuredAccess.GrantOutput.SsmSessionTarget, + NoCache: c.Bool("no-cache"), + }, + DisplayOpts: proxy.DisplayOpts{ + Command: "aws eks proxy", + SessionType: "EKS Proxy", + }, + ConnectionOpts: proxy.ConnectionOpts{ + ServerPort: serverPort, + LocalPort: localPort, + }, + GrantID: ensuredAccess.Grant.Id, + RequestID: ensuredAccess.Grant.AccessRequestId, + }) + if err != nil { + return err + } + } + + // Rather than the user having to specify a port via a flag, the proxy command just grabs an unused port to use. + // it means that each time you run the + tempPort, err := proxy.GrabUnusedPort() + if err != nil { + return err + } + + underlyingProxyServerConn, yamuxStreamConnection, err := proxy.InitiateSessionConnection(cfg, proxy.InitiateSessionConnectionInput{ + GrantID: ensuredAccess.Grant.Id, + RequestURL: requestURL, + LocalPort: localPort, + }) + if err != nil { + return err + } + defer underlyingProxyServerConn.Close() + defer yamuxStreamConnection.Close() + + err = AddContextToConfig(ensuredAccess, tempPort) + if err != nil { + return err + } + + return proxy.ListenAndProxy(ctx, yamuxStreamConnection, tempPort, requestURL) + }, +} + +// promptForClusterAndRole lists all available eks cluster entitlements for the user and displays a table selector UI +func promptForClusterAndRole(ctx context.Context, cfg *config.Context) (*accessv1alpha1.Entitlement, error) { + accessClient := access.NewFromConfig(cfg) + entitlements, err := grab.AllPages(ctx, func(ctx context.Context, nextToken *string) ([]*accessv1alpha1.Entitlement, *string, error) { + res, err := accessClient.QueryEntitlements(ctx, connect.NewRequest(&accessv1alpha1.QueryEntitlementsRequest{ + PageToken: grab.Value(nextToken), + TargetType: grab.Ptr("AWS::EKS::Cluster"), + })) + if err != nil { + return nil, nil, err + } + return res.Msg.Entitlements, &res.Msg.NextPageToken, nil + }) + if err != nil { + return nil, err + } + + // check here to avoid nil pointer errors later + if len(entitlements) == 0 { + return nil, errors.New("you don't have access to any EKS Clusters") + } + + type Column struct { + Title string + Width int + } + cols := []Column{{Title: "Cluster", Width: 40}, {Title: "Role", Width: 40}} + var s = make([]string, 0, len(cols)) + for _, col := range cols { + style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) + renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) + s = append(s, lipgloss.NewStyle().Bold(true).Padding(0).Render(renderedCell)) + } + header := lipgloss.NewStyle().PaddingLeft(2).Render(lipgloss.JoinHorizontal(lipgloss.Left, s...)) + var options []huh.Option[*accessv1alpha1.Entitlement] + + for _, entitlement := range entitlements { + style := lipgloss.NewStyle().Width(cols[0].Width).MaxWidth(cols[0].Width).Inline(true) + target := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Target.Display(), cols[0].Width, "…"))) + + style = lipgloss.NewStyle().Width(cols[1].Width).MaxWidth(cols[1].Width).Inline(true) + role := lipgloss.NewStyle().Bold(true).Padding(0).Render(style.Render(runewidth.Truncate(entitlement.Role.Display(), cols[1].Width, "…"))) + + options = append(options, huh.Option[*accessv1alpha1.Entitlement]{ + Key: lipgloss.JoinHorizontal(lipgloss.Left, target, role), + Value: entitlement, + }) + } + + selector := huh.NewSelect[*accessv1alpha1.Entitlement](). + // show the filter dialog when there are 2 or more options + Filtering(len(options) > 1). + Options(options...). + Title("Select a cluster to connect to"). + Description(header).WithTheme(huh.ThemeBase()) + + err = selector.Run() + if err != nil { + return nil, err + } + + selectorVal := selector.GetValue() + + if selectorVal == nil { + return nil, errors.New("no cluster selected") + } + + return selectorVal.(*accessv1alpha1.Entitlement), nil +} diff --git a/pkg/granted/entrypoint.go b/pkg/granted/entrypoint.go index e2bc9c09..413e21c2 100644 --- a/pkg/granted/entrypoint.go +++ b/pkg/granted/entrypoint.go @@ -14,6 +14,7 @@ import ( "github.com/common-fate/granted/pkg/config" "github.com/common-fate/granted/pkg/granted/auth" "github.com/common-fate/granted/pkg/granted/doctor" + "github.com/common-fate/granted/pkg/granted/eks" "github.com/common-fate/granted/pkg/granted/exp" "github.com/common-fate/granted/pkg/granted/middleware" "github.com/common-fate/granted/pkg/granted/rds" @@ -64,6 +65,7 @@ func GetCliApp() *cli.App { &doctor.Command, &rds.Command, &CFCommand, + &eks.Command, }, // Granted may be invoked via our browser extension, which uses the Native Messaging // protocol to communicate with the Granted CLI. If invoked this way, the browser calls diff --git a/pkg/granted/proxy/ensureaccess.go b/pkg/granted/proxy/ensureaccess.go new file mode 100644 index 00000000..459fcac3 --- /dev/null +++ b/pkg/granted/proxy/ensureaccess.go @@ -0,0 +1,125 @@ +package proxy + +import ( + "context" + "errors" + "time" + + "connectrpc.com/connect" + "github.com/common-fate/clio" + "github.com/common-fate/granted/pkg/hook/accessrequesthook" + "github.com/common-fate/sdk/config" + accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" + "github.com/common-fate/sdk/service/access/grants" + sethRetry "github.com/sethvargo/go-retry" + "google.golang.org/protobuf/types/known/durationpb" +) + +func durationOrDefault(duration time.Duration) *durationpb.Duration { + var out *durationpb.Duration + if duration != 0 { + out = durationpb.New(duration) + } + return out +} + +type EnsureAccessInput[T any] struct { + Target string + Role string + Duration time.Duration + Reason string + Confirm bool + Wait bool + PromptForEntitlement func(ctx context.Context, cfg *config.Context) (*accessv1alpha1.Entitlement, error) + GetGrantOutput func(msg *accessv1alpha1.GetGrantOutputResponse) (T, error) +} +type EnsureAccessOutput[T any] struct { + GrantOutput T + Grant *accessv1alpha1.Grant +} + +// ensureAccess checks for an existing grant or creates a new one if it does not exist +func EnsureAccess[T any](ctx context.Context, cfg *config.Context, input EnsureAccessInput[T]) (*EnsureAccessOutput[T], error) { + + accessRequestInput := accessrequesthook.NoEntitlementAccessInput{ + Target: input.Target, + Role: input.Role, + Reason: input.Reason, + Duration: durationOrDefault(input.Duration), + Confirm: input.Confirm, + Wait: input.Wait, + StartTime: time.Now(), + } + + if accessRequestInput.Target == "" && accessRequestInput.Role == "" { + selectedEntitlement, err := input.PromptForEntitlement(ctx, cfg) + if err != nil { + return nil, err + } + clio.Debugw("selected target and role manually", "selectedEntitlement", selectedEntitlement) + accessRequestInput.Target = selectedEntitlement.Target.Eid.Display() + accessRequestInput.Role = selectedEntitlement.Role.Eid.Display() + } + + hook := accessrequesthook.Hook{} + retry, result, _, err := hook.NoEntitlementAccess(ctx, cfg, accessRequestInput) + if err != nil { + return nil, err + } + + retryDuration := time.Minute * 1 + if input.Wait { + //if wait is specified, increase the timeout to 15 minutes. + retryDuration = time.Minute * 15 + } + + if retry { + // reset the start time for the timer (otherwise it shows 2s, 7s, 12s etc) + accessRequestInput.StartTime = time.Now() + + b := sethRetry.NewConstant(5 * time.Second) + b = sethRetry.WithMaxDuration(retryDuration, b) + err = sethRetry.Do(ctx, b, func(ctx context.Context) (err error) { + + //also proactively check if request has been approved and attempt to activate + result, err = hook.RetryNoEntitlementAccess(ctx, cfg, accessRequestInput) + if err != nil { + + return sethRetry.RetryableError(err) + } + + return nil + }) + if err != nil { + return nil, err + } + + } + + if result == nil || len(result.Grants) == 0 { + return nil, errors.New("could not load grant from Common Fate") + } + + grant := result.Grants[0] + + grantsClient := grants.NewFromConfig(cfg) + + grantOutput, err := grantsClient.GetGrantOutput(ctx, connect.NewRequest(&accessv1alpha1.GetGrantOutputRequest{ + Id: grant.Grant.Id, + })) + if err != nil { + return nil, err + } + + clio.Debugw("found grant output", "output", grantOutput) + + grantOutputFromRes, err := input.GetGrantOutput(grantOutput.Msg) + if err != nil { + return nil, err + } + + return &EnsureAccessOutput[T]{ + GrantOutput: grantOutputFromRes, + Grant: grant.Grant, + }, nil +} diff --git a/pkg/granted/proxy/initiateconnection.go b/pkg/granted/proxy/initiateconnection.go new file mode 100644 index 00000000..49e28ab4 --- /dev/null +++ b/pkg/granted/proxy/initiateconnection.go @@ -0,0 +1,55 @@ +package proxy + +import ( + "fmt" + "net" + + "github.com/common-fate/clio" + "github.com/common-fate/clio/clierr" + "github.com/common-fate/sdk/config" + "github.com/common-fate/sdk/handshake" + "github.com/hashicorp/yamux" +) + +type InitiateSessionConnectionInput struct { + GrantID string + RequestURL string + LocalPort string +} + +// InitiateSessionConnection starts a new tcp connection to through the SSM port forward and completes a handshake with the proxy server +// the result is a yamux session which is used to multiplex client connections +func InitiateSessionConnection(cfg *config.Context, input InitiateSessionConnectionInput) (net.Conn, *yamux.Session, error) { + + // First dial the local SSM portforward, which will be running on a randomly chosen port + // or the local proxy server instance if it's local dev mode + // this establishes the initial connection to the Proxy server + clio.Debugw("dialing proxy server", "host", "localhost:"+input.LocalPort) + rawServerConn, err := net.Dial("tcp", "localhost:"+input.LocalPort) + if err != nil { + return nil, nil, clierr.New("failed to establish a connection to the remote proxy server", clierr.Error(err), clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", input.RequestURL)) + } + // Next, a handshake is performed between the cli client and the Proxy server + // this handshake establishes the users identity to the Proxy, and also the validity of a Database grant + handshaker := handshake.NewHandshakeClient(rawServerConn, input.GrantID, cfg.TokenSource) + handshakeResult, err := handshaker.Handshake() + if err != nil { + return nil, nil, clierr.New("failed to authenticate connection to the remote proxy server", clierr.Error(err), clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", input.RequestURL)) + } + clio.Debugw("handshakeResult", "result", handshakeResult) + + // When the handshake process has completed successfully, we use yamux to establish a multiplexed stream over the existing connection + // We use a multiplexed stream here so that multiple clients can be connected and have their logs attributed to the same session in our audit trail + // To the clients, this is completely opaque + multiplexedServerClient, err := yamux.Client(rawServerConn, nil) + if err != nil { + return nil, nil, err + } + + // Sanity check to confirm that the multiplexed stream is working + _, err = multiplexedServerClient.Ping() + if err != nil { + return nil, nil, fmt.Errorf("failed to healthcheck the network connection to the proxy server: %w", err) + } + return rawServerConn, multiplexedServerClient, nil +} diff --git a/pkg/granted/proxy/listenandproxy.go b/pkg/granted/proxy/listenandproxy.go new file mode 100644 index 00000000..38e9f416 --- /dev/null +++ b/pkg/granted/proxy/listenandproxy.go @@ -0,0 +1,95 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "net" + + "github.com/common-fate/clio" + "github.com/common-fate/clio/clierr" + "github.com/hashicorp/yamux" + "go.uber.org/zap" +) + +// ListenAndProxy will listen for new client connections and start a stream over the established proxy server session. +// if the proxy server terminates the session, like when a grant expires, this listener will detect it and terminate the CLI commmand with an error explaining what happened +func ListenAndProxy(ctx context.Context, yamuxStreamConnection *yamux.Session, clientConnectionPort string, requestURL string) error { + ln, err := net.Listen("tcp", "localhost:"+clientConnectionPort) + if err != nil { + return fmt.Errorf("failed to start listening for connections on port: %s. %w", clientConnectionPort, err) + } + defer ln.Close() + + type result struct { + conn net.Conn + err error + } + resultChan := make(chan result, 100) + go func() { + for { + select { + case <-ctx.Done(): + return + default: + conn, err := ln.Accept() + result := result{ + err: err, + } + if err == nil { + result.conn = conn + } + resultChan <- result + } + } + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-yamuxStreamConnection.CloseChan(): + return clierr.New("The connection to the proxy server has ended", clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", requestURL)) + case result := <-resultChan: + if result.err != nil { + return fmt.Errorf("failed to accept connection: %w", err) + } + if yamuxStreamConnection.IsClosed() { + return clierr.New("failed to accept connection for client because the proxy server connection has ended", clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", requestURL)) + } + go func(clientConn net.Conn) { + // A stream is opened for this connection, streams are used just like a net.Conn and can read and write data + // A stream can only be opened while the grant is still valid, and each new connection will validate the parameters + sessionConn, err := yamuxStreamConnection.OpenStream() + if err != nil { + clio.Error("Failed to establish a new connection to the remote via the proxy server.") + clio.Error(err) + clio.Infof("Your grant may have expired, you can check the status here: %s", requestURL) + return + } + + clio.Infof("Connection accepted for session [%v]", sessionConn.StreamID()) + + // If a stream successfully connects, that means that a connection to the target is now open + // at this point the connection traffic is handed off and the connection is effectively directly from the client and the target + // with queries being intercepted and logged to the audit trail in Common Fate + // if the grant becomes incative at any time the connection is terminated immediately + go func() { + defer sessionConn.Close() + _, err := io.Copy(sessionConn, clientConn) + if err != nil { + clio.Debugw("error writing data from client to server usually this is just because the proxy session ended.", "streamId", sessionConn.StreamID(), zap.Error(err)) + } + clio.Infof("Connection ended for session [%v]", sessionConn.StreamID()) + }() + go func() { + defer sessionConn.Close() + _, err := io.Copy(clientConn, sessionConn) + if err != nil { + clio.Debugw("error writing data from server to client usually this is just because the proxy session ended.", "streamId", sessionConn.StreamID(), zap.Error(err)) + } + }() + }(result.conn) + } + } +} diff --git a/pkg/granted/proxy/ports.go b/pkg/granted/proxy/ports.go new file mode 100644 index 00000000..cca58d7b --- /dev/null +++ b/pkg/granted/proxy/ports.go @@ -0,0 +1,38 @@ +package proxy + +import ( + "net" + "strconv" +) + +// Returns the proxy port to connect to and a local port to send client connections to +// in production, an SSM portforward process is running which is used to connect to the proxy server +// and over the top of this connection, a handshake process takes place and connection multiplexing is used to handle multiple database clients +func Ports(isLocalMode bool) (serverPort, localPort string, err error) { + // in local mode the SSM port forward is not used can skip using ssm and just use a local port forward instead + if isLocalMode { + return "7070", "7070", nil + } + // find an unused local port to use for the ssm server + // the user doesn't directly connect to this, they connect through our local proxy + // which adds authentication + ssmPortforwardLocalPort, err := GrabUnusedPort() + if err != nil { + return "", "", err + } + return "8080", ssmPortforwardLocalPort, nil +} + +func GrabUnusedPort() (string, error) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return "", err + } + + port := listener.Addr().(*net.TCPAddr).Port + err = listener.Close() + if err != nil { + return "", err + } + return strconv.Itoa(port), nil +} diff --git a/pkg/granted/proxy/proxy.go b/pkg/granted/proxy/proxy.go new file mode 100644 index 00000000..9cd3bdcc --- /dev/null +++ b/pkg/granted/proxy/proxy.go @@ -0,0 +1,159 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "os" + "time" + + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/session-manager-plugin/src/datachannel" + "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session" + "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session/portsession" + "github.com/briandowns/spinner" + "github.com/common-fate/clio" + "github.com/common-fate/clio/clierr" + "github.com/common-fate/grab" + "github.com/common-fate/granted/internal/build" + "github.com/common-fate/granted/pkg/cfaws" + + "github.com/common-fate/xid" +) + +type DisplayOpts struct { + //the e.g `aws rds proxy` which is used to fill in a help prompt + Command string + // like `EKS Proxy` or `RDS proxy` + SessionType string +} +type AWSConfig struct { + SSOAccountID string + SSORoleName string + SSORegion string + SSOStartURL string + Region string + SSMSessionTarget string + NoCache bool +} +type ConnectionOpts struct { + ServerPort string + LocalPort string +} +type WaitForSSMConnectionToProxyServerOpts struct { + AWSConfig AWSConfig + DisplayOpts DisplayOpts + ConnectionOpts ConnectionOpts + GrantID string + RequestID string +} + +// WaitForSSMConnectionToProxyServer starts a session with SSM and waits for the connection to be ready +func WaitForSSMConnectionToProxyServer(ctx context.Context, opts WaitForSSMConnectionToProxyServerOpts) error { + + p := &cfaws.Profile{ + Name: opts.GrantID, + ProfileType: "AWS_SSO", + AWSConfig: awsConfig.SharedConfig{ + SSOAccountID: opts.AWSConfig.SSOAccountID, + SSORoleName: opts.AWSConfig.SSORoleName, + SSORegion: opts.AWSConfig.SSORegion, + SSOStartURL: opts.AWSConfig.SSOStartURL, + }, + Initialised: true, + } + + creds, err := p.AssumeTerminal(ctx, cfaws.ConfigOpts{ + ShouldRetryAssuming: grab.Ptr(true), + DisableCache: opts.AWSConfig.NoCache, + }) + if err != nil { + return err + } + + ssmReadyForConnectionsChan := make(chan struct{}) + + awscfg, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken))) + if err != nil { + return err + } + awscfg.Region = opts.AWSConfig.Region + ssmClient := ssm.NewFromConfig(awscfg) + + var sessionOutput *ssm.StartSessionOutput + + documentName := "AWS-StartPortForwardingSession" + startSessionInput := ssm.StartSessionInput{ + Target: &opts.AWSConfig.SSMSessionTarget, + DocumentName: &documentName, + Parameters: map[string][]string{ + "portNumber": {opts.ConnectionOpts.ServerPort}, + "localPortNumber": {opts.ConnectionOpts.LocalPort}, + }, + Reason: grab.Ptr(fmt.Sprintf("Session started for Granted %s connection with Common Fate. GrantID: %s, AccessRequestID: %s", opts.DisplayOpts.SessionType, opts.GrantID, opts.RequestID)), + } + + sessionOutput, err = ssmClient.StartSession(ctx, &startSessionInput) + if err != nil { + return clierr.New("Failed to start AWS SSM port forward session", + clierr.Error(err), + clierr.Infof("You can try re-running this command with the verbose flag to see detailed logs, '%s --verbose %s'", build.GrantedBinaryName(), opts.DisplayOpts.Command), + clierr.Infof("In rare cases, where the proxy service has been re-deployed while your grant was active, you will need to close your request in Common Fate and request access again 'cf access close request --id=%s' This is usually indicated by an error message containing '(TargetNotConnected) when calling the StartSession'", opts.RequestID)) + } + + clientId := xid.New("gtd") + ssmSession := session.Session{ + StreamUrl: *sessionOutput.StreamUrl, + SessionId: *sessionOutput.SessionId, + TokenValue: *sessionOutput.TokenValue, + IsAwsCliUpgradeNeeded: false, + Endpoint: "localhost:" + opts.ConnectionOpts.LocalPort, + DataChannel: &datachannel.DataChannel{}, + ClientId: clientId, + } + + startingProxySpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + startingProxySpinner.Suffix = fmt.Sprintf(" Starting %s...", opts.DisplayOpts.SessionType) + startingProxySpinner.Writer = os.Stderr + startingProxySpinner.Start() + defer startingProxySpinner.Stop() + + // registers the PortSession feature within the ssm library + _ = portsession.PortSession{} + + // the SSMDebugLogger serves two purposes here + // 1. writes ssm session logs to clio.Debug which can be viewed using the --verbose flag + // 2. scans the output for the string "Waiting for connections..." which indicates that the SSM connection was successful + // The notifier will notify the ssmReadyForConnectionsChan which means we can connect to the proxy to complete the initial handshake + ssmLogger := &SSMDebugLogger{ + Writers: []io.Writer{ + &NotifyOnSubstringMatchWriter{ + Phrase: "Waiting for connections...", + Callback: func() { ssmReadyForConnectionsChan <- struct{}{} }, + }, + DebugWriter{}, + }, + } + + // Connect to the Proxy server using SSM + go func() { + // Execute starts the ssm connection + err = ssmSession.Execute(ssmLogger) + if err != nil { + clio.Error("AWS SSM port forward session closed with an error") + clio.Error(err) + clio.Info("You can try re-running this command with the verbose flag to see detailed logs, '%s --verbose %s'", build.GrantedBinaryName(), opts.DisplayOpts.Command) + clio.Infof("In rare cases, where the proxy service has been re-deployed while your grant was active, you will need to close your request in Common Fate and request access again 'cf access close request --id=%s' This is usually indicated by an error message containing '(TargetNotConnected) when calling the StartSession'", opts.RequestID) + } + }() + + // waits for the ssm session to start or context to be cancelled + select { + case <-ssmReadyForConnectionsChan: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/pkg/granted/rds/ssm_logger.go b/pkg/granted/proxy/ssm_logger.go similarity index 99% rename from pkg/granted/rds/ssm_logger.go rename to pkg/granted/proxy/ssm_logger.go index b98dc26d..fb23dcb1 100644 --- a/pkg/granted/rds/ssm_logger.go +++ b/pkg/granted/proxy/ssm_logger.go @@ -1,4 +1,4 @@ -package rds +package proxy import ( "fmt" diff --git a/pkg/granted/rds/writers.go b/pkg/granted/proxy/writers.go similarity index 97% rename from pkg/granted/rds/writers.go rename to pkg/granted/proxy/writers.go index d9c48142..b41e6136 100644 --- a/pkg/granted/rds/writers.go +++ b/pkg/granted/proxy/writers.go @@ -1,4 +1,4 @@ -package rds +package proxy import ( "strings" diff --git a/pkg/granted/rds/rds.go b/pkg/granted/rds/rds.go index 193b6eee..a15283fa 100644 --- a/pkg/granted/rds/rds.go +++ b/pkg/granted/rds/rds.go @@ -4,44 +4,22 @@ import ( "context" "errors" "fmt" - "io" - "net" - "net/url" - "os" - "strconv" - "time" "connectrpc.com/connect" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/ssm" - awsConfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/session-manager-plugin/src/datachannel" - "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session" - "github.com/aws/session-manager-plugin/src/sessionmanagerplugin/session/portsession" - "github.com/briandowns/spinner" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/common-fate/clio" - "github.com/common-fate/clio/clierr" "github.com/common-fate/grab" - "github.com/common-fate/granted/pkg/cfaws" - "github.com/common-fate/granted/pkg/hook/accessrequesthook" + "github.com/common-fate/granted/pkg/cfcfg" + "github.com/common-fate/granted/pkg/granted/proxy" "github.com/common-fate/sdk/config" accessv1alpha1 "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1" - "github.com/common-fate/sdk/gen/commonfate/access/v1alpha1/accessv1alpha1connect" - "github.com/common-fate/sdk/handshake" "github.com/common-fate/sdk/service/access" - "github.com/common-fate/sdk/service/access/grants" - "github.com/common-fate/xid" "github.com/fatih/color" - "github.com/hashicorp/yamux" "github.com/mattn/go-runewidth" - sethRetry "github.com/sethvargo/go-retry" "github.com/urfave/cli/v2" - "go.uber.org/zap" - "google.golang.org/protobuf/types/known/durationpb" ) var Command = cli.Command{ @@ -51,13 +29,18 @@ var Command = cli.Command{ Subcommands: []*cli.Command{&proxyCommand}, } +// isLocalMode is used where some behaviour needs to be changed to run against a local development proxy server +func isLocalMode(c *cli.Context) bool { + return c.String("mode") == "local" +} + var proxyCommand = cli.Command{ Name: "proxy", Usage: "The Proxy plugin is used in conjunction with a Commnon Fate deployment to request temporary access to an AWS RDS Database", Flags: []cli.Flag{ &cli.StringFlag{Name: "target", Aliases: []string{"database"}}, &cli.StringFlag{Name: "role", Aliases: []string{"user"}}, - &cli.IntFlag{Name: "port", Usage: "The local port to forward the mysql database connection to"}, + &cli.IntFlag{Name: "port", Usage: "The local port to forward the database connection to"}, &cli.StringFlag{Name: "reason", Usage: "Provide a reason for requesting access to the role"}, &cli.BoolFlag{Name: "confirm", Aliases: []string{"y"}, Usage: "Skip confirmation prompts for access requests"}, &cli.BoolFlag{Name: "wait", Value: true, Usage: "Wait for the access request to be approved."}, @@ -77,39 +60,68 @@ var proxyCommand = cli.Command{ return err } - ensuredAccess, err := ensureAccess(ctx, cfg, ensureAccessInput{ - Database: c.String("target"), - User: c.String("role"), - Duration: c.Duration("duration"), - Reason: c.String("reason"), - Confirm: c.Bool("confirm"), - Wait: c.Bool("wait"), + ensuredAccess, err := proxy.EnsureAccess(ctx, cfg, proxy.EnsureAccessInput[*accessv1alpha1.AWSRDSOutput]{ + Target: c.String("target"), + Role: c.String("role"), + Duration: c.Duration("duration"), + Reason: c.String("reason"), + Confirm: c.Bool("confirm"), + Wait: c.Bool("wait"), + PromptForEntitlement: promptForDatabaseAndUser, + GetGrantOutput: func(msg *accessv1alpha1.GetGrantOutputResponse) (*accessv1alpha1.AWSRDSOutput, error) { + output := msg.GetOutputAwsRds() + if output == nil { + return nil, errors.New("unexpected grant output, this indicates an error in the Common Fate Provisioning process, you should contect your Common Fate administrator") + } + return output, nil + }, }) if err != nil { return err } - requestURL, err := generateRequestURL(ctx, ensuredAccess.Grant) + requestURL, err := cfcfg.GenerateRequestURL(cfg.APIURL, ensuredAccess.Grant.AccessRequestId) if err != nil { return err } - serverPort, localPort, err := ports(c) + serverPort, localPort, err := proxy.Ports(isLocalMode(c)) if err != nil { return err } clio.Debugw("prepared ports for access", "serverPort", serverPort, "localPort", localPort) - - err = waitForSSMConnectionToProxyServer(c, ensuredAccess, serverPort, localPort) - if err != nil { - return err + if !isLocalMode(c) { + err = proxy.WaitForSSMConnectionToProxyServer(ctx, proxy.WaitForSSMConnectionToProxyServerOpts{ + AWSConfig: proxy.AWSConfig{ + SSOAccountID: ensuredAccess.GrantOutput.RdsDatabase.AccountId, + SSORoleName: ensuredAccess.Grant.Id, + SSORegion: ensuredAccess.GrantOutput.SsoRegion, + SSOStartURL: ensuredAccess.GrantOutput.SsoStartUrl, + Region: ensuredAccess.GrantOutput.RdsDatabase.Region, + SSMSessionTarget: ensuredAccess.GrantOutput.SsmSessionTarget, + NoCache: c.Bool("no-cache"), + }, + DisplayOpts: proxy.DisplayOpts{ + Command: "aws rds proxy", + SessionType: "RDS Proxy", + }, + ConnectionOpts: proxy.ConnectionOpts{ + ServerPort: serverPort, + LocalPort: localPort, + }, + GrantID: ensuredAccess.Grant.Id, + RequestID: ensuredAccess.Grant.AccessRequestId, + }) + if err != nil { + return err + } } - underlyingProxyServerConn, yamuxStreamConnection, err := initiateSessionConnection(cfg, initiateSessionConnectionInput{ - EnsuredAccess: ensuredAccess, - RequestURL: requestURL, - LocalPort: localPort, + underlyingProxyServerConn, yamuxStreamConnection, err := proxy.InitiateSessionConnection(cfg, proxy.InitiateSessionConnectionInput{ + GrantID: ensuredAccess.Grant.Id, + RequestURL: requestURL, + LocalPort: localPort, }) if err != nil { return err @@ -124,94 +136,13 @@ var proxyCommand = cli.Command{ printConnectionParameters(connectionString, cliString, clientConnectionPort, ensuredAccess.GrantOutput.RdsDatabase.Engine) - return listenAndProxy(ctx, yamuxStreamConnection, clientConnectionPort, requestURL) + return proxy.ListenAndProxy(ctx, yamuxStreamConnection, clientConnectionPort, requestURL) }, } -// listenAndProxy will listen for new client connections and start a stream over the established proxy server session. -// if the proxy server terminates the session, like when a grant expires, this listener will detect it and terminate the CLI commmand with an error explaining what happened -func listenAndProxy(ctx context.Context, yamuxStreamConnection *yamux.Session, clientConnectionPort string, requestURL string) error { - ln, err := net.Listen("tcp", "localhost:"+clientConnectionPort) - if err != nil { - return fmt.Errorf("failed to start listening for connections on port: %s. %w", clientConnectionPort, err) - } - defer ln.Close() - - type result struct { - conn net.Conn - err error - } - resultChan := make(chan result, 100) - go func() { - for { - select { - case <-ctx.Done(): - return - default: - conn, err := ln.Accept() - result := result{ - err: err, - } - if err == nil { - result.conn = conn - } - resultChan <- result - } - } - }() - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-yamuxStreamConnection.CloseChan(): - return clierr.New("The connection to the proxy server has ended", clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", requestURL)) - case result := <-resultChan: - if result.err != nil { - return fmt.Errorf("failed to accept connection: %w", err) - } - if yamuxStreamConnection.IsClosed() { - return clierr.New("failed to accept connection for database client because the proxy server connection has ended", clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", requestURL)) - } - go func(databaseClientConn net.Conn) { - // A stream is opened for this connection, streams are used just like a net.Conn and can read and write data - // A stream can only be opened while the grant is still valid, and each new connection will validate the database parameters username and database - sessionConn, err := yamuxStreamConnection.OpenStream() - if err != nil { - clio.Error("Failed to establish a new connection to the remote database via the proxy server.") - clio.Error(err) - clio.Infof("Your grant may have expired, you can check the status here: %s", requestURL) - return - } - - clio.Infof("Connection accepted for session [%v]", sessionConn.StreamID()) - - // If a stream successfully connects, that means that a connection to the target database is now open - // at this point the connection traffic is handed off and the connection is effectively directly from the database client and the target database - // with queries being intercepted and logged to the audit trail in Common Fate - // if the grant becomes incative at any time the connection is terminated immediately - go func() { - defer sessionConn.Close() - _, err := io.Copy(sessionConn, databaseClientConn) - if err != nil { - clio.Debugw("error writing data from client to server usually this is just because the database proxy session ended.", "streamId", sessionConn.StreamID(), zap.Error(err)) - } - clio.Infof("Connection ended for session [%v]", sessionConn.StreamID()) - }() - go func() { - defer sessionConn.Close() - _, err := io.Copy(databaseClientConn, sessionConn) - if err != nil { - clio.Debugw("error writing data from server to client usually this is just because the database proxy session ended.", "streamId", sessionConn.StreamID(), zap.Error(err)) - } - }() - }(result.conn) - } - } -} - // promptForDatabaseAndUser lists all available database entitlements for the user and displays a table selector UI -func promptForDatabaseAndUser(ctx context.Context, accessClient accessv1alpha1connect.AccessServiceClient) (*accessv1alpha1.Entitlement, error) { +func promptForDatabaseAndUser(ctx context.Context, cfg *config.Context) (*accessv1alpha1.Entitlement, error) { + accessClient := access.NewFromConfig(cfg) entitlements, err := grab.AllPages(ctx, func(ctx context.Context, nextToken *string) ([]*accessv1alpha1.Entitlement, *string, error) { res, err := accessClient.QueryEntitlements(ctx, connect.NewRequest(&accessv1alpha1.QueryEntitlementsRequest{ PageToken: grab.Value(nextToken), @@ -279,294 +210,7 @@ func promptForDatabaseAndUser(ctx context.Context, accessClient accessv1alpha1co return selectorVal.(*accessv1alpha1.Entitlement), nil } -func durationOrDefault(duration time.Duration) *durationpb.Duration { - var out *durationpb.Duration - if duration != 0 { - out = durationpb.New(duration) - } - return out -} - -type ensureAccessInput struct { - Database string - User string - Duration time.Duration - Reason string - Confirm bool - Wait bool -} -type ensureAccessOutput struct { - GrantOutput *accessv1alpha1.AWSRDSOutput - Grant *accessv1alpha1.Grant -} - -// ensureAccess checks for an existing grant or creates a new one if it does not exist -func ensureAccess(ctx context.Context, cfg *config.Context, input ensureAccessInput) (*ensureAccessOutput, error) { - - accessRequestInput := accessrequesthook.NoEntitlementAccessInput{ - Target: input.Database, - Role: input.User, - Reason: input.Reason, - Duration: durationOrDefault(input.Duration), - Confirm: input.Confirm, - Wait: input.Wait, - StartTime: time.Now(), - } - - if accessRequestInput.Target == "" && accessRequestInput.Role == "" { - selectedEntitlement, err := promptForDatabaseAndUser(ctx, access.NewFromConfig(cfg)) - if err != nil { - return nil, err - } - clio.Debugw("selected database and user manually", "selectedEntitlement", selectedEntitlement) - accessRequestInput.Target = selectedEntitlement.Target.Eid.Display() - accessRequestInput.Role = selectedEntitlement.Role.Eid.Display() - } - - hook := accessrequesthook.Hook{} - retry, result, _, err := hook.NoEntitlementAccess(ctx, cfg, accessRequestInput) - if err != nil { - return nil, err - } - - retryDuration := time.Minute * 1 - if input.Wait { - //if wait is specified, increase the timeout to 15 minutes. - retryDuration = time.Minute * 15 - } - - if retry { - // reset the start time for the timer (otherwise it shows 2s, 7s, 12s etc) - accessRequestInput.StartTime = time.Now() - - b := sethRetry.NewConstant(5 * time.Second) - b = sethRetry.WithMaxDuration(retryDuration, b) - err = sethRetry.Do(ctx, b, func(ctx context.Context) (err error) { - - //also proactively check if request has been approved and attempt to activate - result, err = hook.RetryNoEntitlementAccess(ctx, cfg, accessRequestInput) - if err != nil { - - return sethRetry.RetryableError(err) - } - - return nil - }) - if err != nil { - return nil, err - } - - } - - if result == nil || len(result.Grants) == 0 { - return nil, errors.New("could not load grant from Common Fate") - } - - grant := result.Grants[0] - - grantsClient := grants.NewFromConfig(cfg) - - grantOutput, err := grantsClient.GetGrantOutput(ctx, connect.NewRequest(&accessv1alpha1.GetGrantOutputRequest{ - Id: grant.Grant.Id, - })) - if err != nil { - return nil, err - } - - clio.Debugw("found grant output", "output", grantOutput) - - rdsOutput, ok := grantOutput.Msg.Output.(*accessv1alpha1.GetGrantOutputResponse_OutputAwsRds) - if !ok { - return nil, errors.New("unexpected grant output, this indicates an error in the Common Fate Provisioning process, you should contect your Common Fate administrator") - } - - return &ensureAccessOutput{ - GrantOutput: rdsOutput.OutputAwsRds, - Grant: grant.Grant, - }, nil -} - -// isLocalMode is used where some behaviour needs to be changed to run against a local development proxy server -func isLocalMode(c *cli.Context) bool { - return c.String("mode") == "local" -} - -// Returns the proxy port to connect to and a local port to send client connections to -// in production, an SSM portforward process is running which is used to connect to the proxy server -// and over the top of this connection, a handshake process takes place and connection multiplexing is used to handle multiple database clients -func ports(c *cli.Context) (serverPort, localPort string, err error) { - // in local mode the SSM port forward is not used can skip using ssm and just use a local port forward instead - if isLocalMode(c) { - return "7070", "7070", nil - } - // find an unused local port to use for the ssm server - // the user doesn't directly connect to this, they connect through our local proxy - // which adds authentication - ssmPortforwardLocalPort, err := GrabUnusedPort() - if err != nil { - return "", "", err - } - return "8080", ssmPortforwardLocalPort, nil -} - -// waitForSSMConnectionToProxyServer starts a session with SSM and waits for the connection to be ready -func waitForSSMConnectionToProxyServer(c *cli.Context, ensuredAccess *ensureAccessOutput, serverPort, localPort string) error { - // In local mode ssm is not used, instead, the command connects directly to the proxy service running in local dev - // Return early because there is nothing to startup - if isLocalMode(c) { - return nil - } - - ctx := c.Context - - p := &cfaws.Profile{ - Name: ensuredAccess.Grant.Id, - ProfileType: "AWS_SSO", - AWSConfig: awsConfig.SharedConfig{ - SSOAccountID: ensuredAccess.GrantOutput.RdsDatabase.AccountId, - SSORoleName: ensuredAccess.Grant.Id, - SSORegion: ensuredAccess.GrantOutput.SsoRegion, - SSOStartURL: ensuredAccess.GrantOutput.SsoStartUrl, - }, - Initialised: true, - } - - creds, err := p.AssumeTerminal(ctx, cfaws.ConfigOpts{ - ShouldRetryAssuming: grab.Ptr(true), - DisableCache: c.Bool("no-cache"), - }) - if err != nil { - return err - } - - ssmReadyForConnectionsChan := make(chan struct{}) - - awscfg, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken))) - if err != nil { - return err - } - awscfg.Region = ensuredAccess.GrantOutput.RdsDatabase.Region - ssmClient := ssm.NewFromConfig(awscfg) - - var sessionOutput *ssm.StartSessionOutput - - documentName := "AWS-StartPortForwardingSession" - startSessionInput := ssm.StartSessionInput{ - Target: &ensuredAccess.GrantOutput.SsmSessionTarget, - DocumentName: &documentName, - Parameters: map[string][]string{ - "portNumber": {serverPort}, - "localPortNumber": {localPort}, - }, - Reason: grab.Ptr(fmt.Sprintf("Session started for Granted RDS Proxy connection with Common Fate. GrantID: %s, AccessRequestID: %s", ensuredAccess.Grant.Id, ensuredAccess.Grant.AccessRequestId)), - } - - sessionOutput, err = ssmClient.StartSession(ctx, &startSessionInput) - if err != nil { - return clierr.New("Failed to start AWS SSM port forward session", - clierr.Error(err), - clierr.Info("You can try re-running this command with the verbose flag to see detailed logs, 'cf --verbose aws rds proxy'"), - clierr.Infof("In rare cases, where the database proxy has been re-deployed while your grant was active, you will need to close your request in Common Fate and request access again 'cf access close request --id=%s' This is usually indicated by an error message containing '(TargetNotConnected) when calling the StartSession'", ensuredAccess.Grant.AccessRequestId)) - } - - clientId := xid.New("gtd") - ssmSession := session.Session{ - StreamUrl: *sessionOutput.StreamUrl, - SessionId: *sessionOutput.SessionId, - TokenValue: *sessionOutput.TokenValue, - IsAwsCliUpgradeNeeded: false, - Endpoint: "localhost:" + localPort, - DataChannel: &datachannel.DataChannel{}, - ClientId: clientId, - } - - startingProxySpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond) - startingProxySpinner.Suffix = " Starting database proxy..." - startingProxySpinner.Writer = os.Stderr - startingProxySpinner.Start() - defer startingProxySpinner.Stop() - - // registers the PortSession feature within the ssm library - _ = portsession.PortSession{} - - // the SSMDebugLogger serves two purposes here - // 1. writes ssm session logs to clio.Debug which can be viewed using the --verbose flag - // 2. scans the output for the string "Waiting for connections..." which indicates that the SSM connection was successful - // The notifier will notify the ssmReadyForConnectionsChan which means we can connect to the proxy to complete the initial handshake - ssmLogger := &SSMDebugLogger{ - Writers: []io.Writer{ - &NotifyOnSubstringMatchWriter{ - Phrase: "Waiting for connections...", - Callback: func() { ssmReadyForConnectionsChan <- struct{}{} }, - }, - DebugWriter{}, - }, - } - - // Connect to the Proxy server using SSM - go func() { - // Execute starts the ssm connection - err = ssmSession.Execute(ssmLogger) - if err != nil { - clio.Error("AWS SSM port forward session closed with an error") - clio.Error(err) - clio.Info("You can try re-running this command with the verbose flag to see detailed logs, 'cf --verbose aws rds proxy'") - clio.Infof("In rare cases, where the database proxy has been re-deployed while your grant was active, you will need to close your request in Common Fate and request access again 'cf access close request --id=%s' This is usually indicated by an error message containing '(TargetNotConnected) when calling the StartSession'", ensuredAccess.Grant.AccessRequestId) - } - }() - - // waits for the ssm session to start or context to be cancelled - select { - case <-ssmReadyForConnectionsChan: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -type initiateSessionConnectionInput struct { - EnsuredAccess *ensureAccessOutput - RequestURL string - LocalPort string -} - -// initiateSessionConnection starts a new tcp connection to through the SSM port forward and completes a handshake with the proxy server -// the result is a yamux session which is used to multiplex database client connections -func initiateSessionConnection(cfg *config.Context, input initiateSessionConnectionInput) (net.Conn, *yamux.Session, error) { - - // First dial the local SSM portforward, which will be running on a randomly chosen port - // or the local proxy server instance if it's local dev mode - // this establishes the initial connection to the Proxy server - clio.Debugw("dialing proxy server", "host", "localhost:"+input.LocalPort) - rawServerConn, err := net.Dial("tcp", "localhost:"+input.LocalPort) - if err != nil { - return nil, nil, clierr.New("failed to establish a connection to the remote proxy server", clierr.Error(err), clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", input.RequestURL)) - } - // Next, a handshake is performed between the cli client and the Proxy server - // this handshake establishes the users identity to the Proxy, and also the validity of a Database grant - handshaker := handshake.NewHandshakeClient(rawServerConn, input.EnsuredAccess.Grant.Id, cfg.TokenSource) - handshakeResult, err := handshaker.Handshake() - if err != nil { - return nil, nil, clierr.New("failed to authenticate connection to the remote proxy server", clierr.Error(err), clierr.Infof("Your grant may have expired, you can check the status here: %s and retry connecting", input.RequestURL)) - } - clio.Debugw("handshakeResult", "result", handshakeResult) - - // When the handshake process has completed successfully, we use yamux to establish a multiplexed stream over the existing connection - // We use a multiplexed stream here so that multiple database clients can be connected and have their logs attributed to the same session in our audit trail - // To the database clients, this is completely opaque - multiplexedServerClient, err := yamux.Client(rawServerConn, nil) - if err != nil { - return nil, nil, err - } - - // Sanity check to confirm that the multiplexed stream is working - _, err = multiplexedServerClient.Ping() - if err != nil { - return nil, nil, fmt.Errorf("failed to healthcheck the network connection to the proxy server: %w", err) - } - return rawServerConn, multiplexedServerClient, nil -} -func clientConnectionParameters(c *cli.Context, ensuredAccess *ensureAccessOutput) (connectionString, cliString, port string, err error) { +func clientConnectionParameters(c *cli.Context, ensuredAccess *proxy.EnsureAccessOutput[*accessv1alpha1.AWSRDSOutput]) (connectionString, cliString, port string, err error) { // Print the connection information to the user based on the database they are connecting to // the passwords are always 'password' while the username and database will match that of the target being connected to yellow := color.New(color.FgYellow) @@ -592,6 +236,7 @@ func clientConnectionParameters(c *cli.Context, ensuredAccess *ensureAccessOutpu } return } + func printConnectionParameters(connectionString, cliString, port, engine string) { clio.NewLine() clio.Infof("Database proxy ready for connections on 127.0.0.1:%s", port) @@ -603,35 +248,3 @@ func printConnectionParameters(connectionString, cliString, port, engine string) clio.Infof("Or using the %s cli: %s", engine, cliString) clio.NewLine() } - -func generateRequestURL(ctx context.Context, grant *accessv1alpha1.Grant) (string, error) { - cfg, err := config.LoadDefault(ctx) - if err != nil { - return "", err - } - - err = cfg.Initialize(ctx, config.InitializeOpts{}) - if err != nil { - return "", err - } - apiURL, err := url.Parse(cfg.APIURL) - if err != nil { - return "", err - } - p := apiURL.JoinPath("access", "requests", grant.AccessRequestId) - return p.String(), nil -} - -func GrabUnusedPort() (string, error) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - return "", err - } - - port := listener.Addr().(*net.TCPAddr).Port - err = listener.Close() - if err != nil { - return "", err - } - return strconv.Itoa(port), nil -} diff --git a/pkg/hook/accessrequesthook/accessrequesthook.go b/pkg/hook/accessrequesthook/accessrequesthook.go index e7bf339d..059477ce 100644 --- a/pkg/hook/accessrequesthook/accessrequesthook.go +++ b/pkg/hook/accessrequesthook/accessrequesthook.go @@ -458,6 +458,10 @@ func DryRun(ctx context.Context, apiURL *url.URL, client accessv1alpha1connect.A } } + if !confirm { + return false, nil, errors.New("cancelled operation") + } + clio.Info("Attempting to grant access...") return confirm, res.Msg, nil }