diff --git a/go.mod b/go.mod index 9316361c48..e28b9a001d 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/lipgloss v0.11.0 github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df - github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20230822191820-abc0b42e8715 + github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250205171805-d4709871c4f3 github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 github.com/confluentinc/ccloud-sdk-go-v2/billing v0.3.0 diff --git a/go.sum b/go.sum index c2597ea722..2708075f3e 100644 --- a/go.sum +++ b/go.sum @@ -27,7 +27,6 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -72,11 +71,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/actgardner/gogen-avro/v10 v10.2.1 h1:z3pOGblRjAJCYpkIJ8CmbMJdksi4rAhaygw0dyXZ930= @@ -190,8 +186,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/compose-spec/compose-go/v2 v2.1.3 h1:bD67uqLuL/XgkAK6ir3xZvNLFPxPScEi1KW7R5esrLE= github.com/compose-spec/compose-go/v2 v2.1.3/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= -github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20230822191820-abc0b42e8715 h1:ue2NpgHptQApJdrJIOQunF7Jjt4CHeIALTQsguBpWtU= -github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20230822191820-abc0b42e8715/go.mod h1:Ja+ScdwUPpLwPDi/jI4FU2hI4Ncu3o4A4nxo05ehXHQ= +github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250205171805-d4709871c4f3 h1:kAIl8jZxqfa1BQgVG3fjJdnkMtDebf6rIOAo2mnnCLg= +github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250205171805-d4709871c4f3/go.mod h1:lhebz5+uvzoEFI6lMbEyC7ieMggCxG3HT1gyuJ1yCkc= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA= github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw= @@ -305,11 +301,9 @@ github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dghubble/sling v1.4.1/go.mod h1:QoMB1KL3GAo+7HsD8Itd6S+6tW91who8BGZzuLvpOyc= github.com/dghubble/sling v1.4.2 h1:vs1HIGBbSl2SEALyU+irpYFLZMfc49Fp+jYryFebQjM= github.com/dghubble/sling v1.4.2/go.mod h1:o0arCOz0HwfqYQJLrRtqunaWOn4X6jxE/6ORKRpVTD4= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -336,15 +330,11 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -355,12 +345,10 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= 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/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= @@ -368,14 +356,12 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -393,7 +379,6 @@ github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -401,14 +386,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 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.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 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/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 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-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= @@ -424,13 +405,11 @@ github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14j github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 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/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -445,7 +424,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -460,8 +438,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -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/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -484,13 +460,11 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -512,7 +486,6 @@ github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -522,7 +495,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= @@ -566,10 +538,8 @@ github.com/havoc-io/gopass v0.0.0-20170602182606-9a121bec1ae7 h1:ugvine+ipOFuvsb github.com/havoc-io/gopass v0.0.0-20170602182606-9a121bec1ae7/go.mod h1:f9gPqQgJ3af7YNppznMOqzxsLZ79DbHmZ6/lef+LlPY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -592,9 +562,6 @@ github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 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 v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -606,7 +573,6 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -629,12 +595,10 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -701,9 +665,6 @@ github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3 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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -716,18 +677,12 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -744,14 +699,11 @@ github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= @@ -802,7 +754,6 @@ github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= @@ -813,13 +764,10 @@ github.com/sourcegraph/go-lsp v0.0.0-20240223163137-f80c5dd31dfd h1:Dq5WSzWsP1Tb github.com/sourcegraph/go-lsp v0.0.0-20240223163137-f80c5dd31dfd/go.mod h1:SULmZY7YNBsvNiQbrb/BEDdEJ84TGnfyUQxaHt8t8rY= github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U= github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= @@ -828,7 +776,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/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= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -888,7 +835,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= -github.com/travisjeffery/mocker v1.1.0/go.mod h1:ezREEUpF+NpAhAcbduW0dLT4GIkNCZwdcsz0tsaTSCU= github.com/travisjeffery/mocker v1.1.1 h1:CjxXDcIfmeqcOInid+IEVbJMjQTYxGGWs3MnMemgJsM= github.com/travisjeffery/mocker v1.1.1/go.mod h1:ezREEUpF+NpAhAcbduW0dLT4GIkNCZwdcsz0tsaTSCU= github.com/travisjeffery/proto-go-sql v0.0.0-20190911121832-39ff47280e87 h1:PYSpVMo7ihL46eJdNnu1KITaPG7h/YJ0TaSzE9Ijpek= @@ -969,12 +915,10 @@ go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -1005,7 +949,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -1018,14 +961,11 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1036,7 +976,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1061,8 +1000,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= @@ -1077,7 +1014,6 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1095,9 +1031,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1107,7 +1041,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1153,8 +1086,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1164,14 +1095,11 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1180,8 +1108,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -1194,8 +1120,6 @@ golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1247,7 +1171,6 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= @@ -1255,7 +1178,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1354,10 +1276,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -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.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= @@ -1370,19 +1288,15 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/launchdarkly/go-jsonstream.v1 v1.0.1 h1:aZHvMDAS+M6/0sRMkDBQ8MyLGsTQrNgN5evu5e8UYpQ= gopkg.in/launchdarkly/go-jsonstream.v1 v1.0.1/go.mod h1:YefdBjfITIP8D9BJLVbssFctHkJnQXhv+TiRdTV0Jr4= gopkg.in/launchdarkly/go-sdk-common.v2 v2.5.1 h1:RqucG3hCU/GAupuEyVXXPf0Hz3F4InyhiFR2sfUbgBs= gopkg.in/launchdarkly/go-sdk-common.v2 v2.5.1/go.mod h1:P2+C6CHteys+lEDd6298QszCsMhjdYrfzBd6dg//CHA= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -1398,20 +1312,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= -k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= 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-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= @@ -1423,10 +1331,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 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 v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 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.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= tags.cncf.io/container-device-interface v0.7.2 h1:MLqGnWfOr1wB7m08ieI4YJ3IoLKKozEnnNYBtacDPQU= diff --git a/internal/login/command.go b/internal/login/command.go index e2706c1503..701663aa50 100644 --- a/internal/login/command.go +++ b/internal/login/command.go @@ -354,8 +354,8 @@ func (c *command) getURL(cmd *cobra.Command) (string, error) { } func (c *command) saveLoginToKeychain(isCloud bool, url string, credentials *pauth.Credentials) error { - if credentials.IsSSO { - output.ErrPrintln(c.cfg.EnableColor, "The `--save` flag was ignored since SSO credentials are not stored locally.") + if credentials.IsSSO || credentials.IsMFA { + output.ErrPrintln(c.cfg.EnableColor, "The `--save` flag was ignored since SSO or MFA credentials are not stored on keychain.") return nil } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 158323ac61..4f3f1070fb 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -84,7 +84,7 @@ func PersistConfluentLoginToConfig(cfg *config.Config, credentials *Credentials, } ctxName := GenerateContextName(username, url, caCertPath) - return addOrUpdateContext(cfg, false, credentials, ctxName, url, state, caCertPath, "", save) + return addOrUpdateContext(cfg, false, credentials, ctxName, url, state, caCertPath, "", save, false) } func PersistCCloudCredentialsToConfig(config *config.Config, client *ccloudv1.Client, url string, credentials *Credentials, save bool) (string, *ccloudv1.Organization, error) { @@ -97,7 +97,7 @@ func PersistCCloudCredentialsToConfig(config *config.Config, client *ccloudv1.Cl state := getCCloudContextState(credentials.AuthToken, credentials.AuthRefreshToken, user) - if err := addOrUpdateContext(config, true, credentials, ctxName, url, state, "", user.GetOrganization().GetResourceId(), save); err != nil { + if err := addOrUpdateContext(config, true, credentials, ctxName, url, state, "", user.GetOrganization().GetResourceId(), save, credentials.IsMFA); err != nil { return "", nil, err } @@ -112,7 +112,7 @@ func PersistCCloudCredentialsToConfig(config *config.Config, client *ccloudv1.Cl return ctx.CurrentEnvironment, user.GetOrganization(), nil } -func addOrUpdateContext(cfg *config.Config, isCloud bool, credentials *Credentials, ctxName, url string, state *config.ContextState, caCertPath, organizationId string, save bool) error { +func addOrUpdateContext(cfg *config.Config, isCloud bool, credentials *Credentials, ctxName, url string, state *config.ContextState, caCertPath, organizationId string, save, isMFA bool) error { platform := &config.Platform{ Name: strings.TrimSuffix(strings.TrimPrefix(url, "https://"), "/"), Server: url, @@ -171,8 +171,9 @@ func addOrUpdateContext(cfg *config.Config, isCloud bool, credentials *Credentia ctx.Credential = credential ctx.CredentialName = credential.Name ctx.LastOrgId = organizationId + ctx.IsMFA = isMFA } else { - if err := cfg.AddContext(ctxName, platform.Name, credential.Name, map[string]*config.KafkaClusterConfig{}, "", state, organizationId, ""); err != nil { + if err := cfg.AddContext(ctxName, platform.Name, credential.Name, map[string]*config.KafkaClusterConfig{}, "", state, organizationId, "", isMFA); err != nil { return err } } diff --git a/pkg/auth/auth_token_handler.go b/pkg/auth/auth_token_handler.go index 77c8c78deb..4f52f1891a 100644 --- a/pkg/auth/auth_token_handler.go +++ b/pkg/auth/auth_token_handler.go @@ -7,11 +7,13 @@ import ( "strings" "time" + "github.com/gogo/protobuf/types" "github.com/pkg/browser" ccloudv1 "github.com/confluentinc/ccloud-sdk-go-v1-public" "github.com/confluentinc/mds-sdk-go-public/mdsv1" + "github.com/confluentinc/cli/v4/pkg/auth/mfa" "github.com/confluentinc/cli/v4/pkg/auth/sso" "github.com/confluentinc/cli/v4/pkg/errors" "github.com/confluentinc/cli/v4/pkg/form" @@ -40,11 +42,16 @@ func (a *AuthTokenHandlerImpl) GetCCloudTokens(clientFactory CCloudClientFactory if token, refreshToken, err := a.refreshCCloudSSOToken(client, credentials.AuthRefreshToken, organizationId); err == nil { return token, refreshToken, nil } + } else if credentials.IsMFA { + if token, refreshToken, err := a.refreshCCloudMFAToken(client, credentials.AuthRefreshToken, organizationId, credentials.Username); err == nil { + return token, refreshToken, nil + } } else { req := &ccloudv1.AuthenticateRequest{ RefreshToken: credentials.AuthRefreshToken, OrgResourceId: organizationId, } + if res, err := client.Auth.Login(req); err == nil { return res.GetToken(), res.GetRefreshToken(), nil } @@ -63,6 +70,17 @@ func (a *AuthTokenHandlerImpl) GetCCloudTokens(clientFactory CCloudClientFactory return token, refreshToken, err } + if credentials.IsMFA { + token, refreshToken, err := a.getCCloudMFAToken(client, credentials.Username, organizationId) + if err != nil { + return "", "", err + } + + client = clientFactory.JwtHTTPClientFactory(context.Background(), token, url) + err = a.checkMFAEmailMatchesLogin(client, credentials.Username) + return token, refreshToken, err + } + client.HttpClient.Timeout = 30 * time.Second log.CliLogger.Debugf("Making login request for %s for org id %s", credentials.Username, organizationId) @@ -76,6 +94,9 @@ func (a *AuthTokenHandlerImpl) GetCCloudTokens(clientFactory CCloudClientFactory if err != nil { return "", "", err } + if res.GetOrganization().GetMfaEnforcedAt() != nil && res.GetUser().GetAuthType() == ccloudv1.AuthType_AUTH_TYPE_LOCAL { + output.Printf(false, "Please be aware that you will be required to enroll in MFA by %s\n", convertDateFormat(res.GetOrganization().GetMfaEnforcedAt())) + } if utils.IsOrgEndOfFreeTrialSuspended(res.GetOrganization().GetSuspensionStatus()) { log.CliLogger.Debugf(errors.EndOfFreeTrialErrorMsg, res.GetOrganization().GetSuspensionStatus()) @@ -112,6 +133,32 @@ func (a *AuthTokenHandlerImpl) getCCloudSSOToken(client *ccloudv1.Client, noBrow return res.GetToken(), refreshToken, err } +func (a *AuthTokenHandlerImpl) getCCloudMFAToken(client *ccloudv1.Client, email, organizationId string) (string, string, error) { + connectionName, err := a.getMfaConnectionName(client, email, organizationId) + if err != nil { + return "", "", fmt.Errorf(`unable to obtain MFA info for user "%s: %v"`, email, err) + } + if connectionName == "" { + return "", "", fmt.Errorf(`tried to obtain MFA token for non MFA user "%s"`, email) + } + + idToken, refreshToken, err := mfa.Login(client.BaseURL, email, connectionName) + if err != nil { + return "", "", err + } + + req := &ccloudv1.AuthenticateRequest{ + IdToken: idToken, + OrgResourceId: organizationId, + } + res, err := client.Auth.Login(req) + if err != nil { + return "", "", err + } + + return res.GetToken(), refreshToken, err +} + func (a *AuthTokenHandlerImpl) getSsoConnectionName(client *ccloudv1.Client, email, organizationId string) (string, error) { req := &ccloudv1.GetLoginRealmRequest{ Email: email, @@ -128,6 +175,22 @@ func (a *AuthTokenHandlerImpl) getSsoConnectionName(client *ccloudv1.Client, ema return "", nil } +func (a *AuthTokenHandlerImpl) getMfaConnectionName(client *ccloudv1.Client, email, organizationId string) (string, error) { + req := &ccloudv1.GetLoginRealmRequest{ + Email: email, + ClientId: sso.GetAuth0CCloudClientIdFromBaseUrl(client.BaseURL), + OrgResourceId: organizationId, + } + loginRealmReply, err := client.User.LoginRealm(req) + if err != nil { + return "", err + } + if loginRealmReply.GetMfaRequired() { + return loginRealmReply.GetRealm(), nil + } + return "", nil +} + func (a *AuthTokenHandlerImpl) refreshCCloudSSOToken(client *ccloudv1.Client, refreshToken, organizationId string) (string, string, error) { idToken, refreshToken, err := sso.RefreshTokens(client.BaseURL, refreshToken) if err != nil { @@ -146,6 +209,24 @@ func (a *AuthTokenHandlerImpl) refreshCCloudSSOToken(client *ccloudv1.Client, re return res.GetToken(), refreshToken, err } +func (a *AuthTokenHandlerImpl) refreshCCloudMFAToken(client *ccloudv1.Client, refreshToken, organizationId, email string) (string, string, error) { + idToken, refreshToken, err := mfa.RefreshTokens(client.BaseURL, refreshToken, email) + if err != nil { + return "", "", err + } + + req := &ccloudv1.AuthenticateRequest{ + IdToken: idToken, + OrgResourceId: organizationId, + } + + res, err := login(client, req) + if err != nil { + return "", "", err + } + + return res.GetToken(), refreshToken, err +} func (a *AuthTokenHandlerImpl) GetConfluentToken(mdsClient *mdsv1.APIClient, credentials *Credentials, noBrowser bool) (string, string, error) { ctx := utils.GetContext() @@ -226,6 +307,20 @@ func refreshConfluentToken(mdsClient *mdsv1.APIClient, credentials *Credentials) return resp.AuthToken, nil } +func (a *AuthTokenHandlerImpl) checkMFAEmailMatchesLogin(client *ccloudv1.Client, loginEmail string) error { + getMeReply, err := client.Auth.User() + if err != nil { + return err + } + if !strings.EqualFold(getMeReply.GetUser().GetEmail(), loginEmail) { + return errors.NewErrorWithSuggestions( + fmt.Sprintf("expected login credentials for %s but got credentials for %s", loginEmail, getMeReply.GetUser().GetEmail()), + "Please re-login and use the same email at the prompt and in the login portal.", + ) + } + return nil +} + func (a *AuthTokenHandlerImpl) checkSSOEmailMatchesLogin(client *ccloudv1.Client, loginEmail string) error { getMeReply, err := client.Auth.User() if err != nil { @@ -247,3 +342,11 @@ func login(client *ccloudv1.Client, req *ccloudv1.AuthenticateRequest) (*ccloudv return client.Auth.Login(req) } } + +func convertDateFormat(mfaEnforcedAt *types.Timestamp) string { + if mfaEnforcedAt == nil { + return "Invalid Date" + } + date := time.Unix(mfaEnforcedAt.Seconds, int64(mfaEnforcedAt.Nanos)).UTC().Truncate(time.Microsecond) + return date.Format("01/02/2006") +} diff --git a/pkg/auth/auth_token_handler_test.go b/pkg/auth/auth_token_handler_test.go new file mode 100644 index 0000000000..1d0173792c --- /dev/null +++ b/pkg/auth/auth_token_handler_test.go @@ -0,0 +1,31 @@ +package auth + +import ( + "testing" + "time" + + "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/assert" +) + +func TestConvertDateFormatToString(t *testing.T) { + t.Run("success", func(t *testing.T) { + date := time.Date(2021, time.June, 16, 12, 0, 0, 0, time.UTC) + timestamp := &types.Timestamp{Seconds: date.Unix()} + actual := convertDateFormat(timestamp) + expected := "06/16/2021" + assert.Equal(t, expected, actual) + }) + t.Run("fail", func(t *testing.T) { + date := time.Date(2021, time.April, 16, 12, 0, 0, 0, time.UTC) + timestamp := &types.Timestamp{Seconds: date.Unix()} + actual := convertDateFormat(timestamp) + expected := "06/16/2021" + assert.NotEqual(t, expected, actual) + }) + t.Run("fail, nil", func(t *testing.T) { + actual := convertDateFormat(&types.Timestamp{}) + expected := "01/01/1970" + assert.Equal(t, expected, actual) + }) +} diff --git a/pkg/auth/login_credentials_manager.go b/pkg/auth/login_credentials_manager.go index 7f975187cd..4a70929c74 100644 --- a/pkg/auth/login_credentials_manager.go +++ b/pkg/auth/login_credentials_manager.go @@ -28,6 +28,7 @@ type Credentials struct { Username string Password string IsSSO bool + IsMFA bool Salt []byte Nonce []byte @@ -40,7 +41,7 @@ type Credentials struct { } func (c *Credentials) IsFullSet() bool { - return c.Username != "" && (c.IsSSO || c.Password != "" || c.AuthRefreshToken != "") + return c.Username != "" && (c.IsSSO || c.IsMFA || c.Password != "" || c.AuthRefreshToken != "") } type environmentVariables struct { @@ -108,6 +109,9 @@ func (h *LoginCredentialsManagerImpl) getCredentialsFromEnvVarFunc(envVars envir if h.isSSOUser(email, organizationId) { log.CliLogger.Debugf("%s=%s belongs to an SSO user.", ConfluentCloudEmail, email) return &Credentials{Username: email, IsSSO: true}, nil + } else if h.isMFARequired(email, organizationId) { + log.CliLogger.Debugf("%s=%s belongs to an MFA user.", ConfluentCloudEmail, email) + return &Credentials{Username: email, IsMFA: true}, nil } if password == "" { @@ -182,6 +186,7 @@ func (h *LoginCredentialsManagerImpl) GetPrerunCredentialsFromConfig(cfg *config credentials := &Credentials{ IsSSO: ctx.GetUser().GetAuthType() == ccloudv1.AuthType_AUTH_TYPE_SSO || ctx.GetUser().GetSocialConnection() != "", + IsMFA: ctx.IsMFA, Username: ctx.GetUser().GetEmail(), AuthToken: ctx.GetAuthToken(), AuthRefreshToken: ctx.GetAuthRefreshToken(), @@ -245,6 +250,8 @@ func (h *LoginCredentialsManagerImpl) GetCloudCredentialsFromPrompt(organization if h.isSSOUser(email, organizationId) { log.CliLogger.Debug("Entered email belongs to an SSO user.") return &Credentials{Username: email, IsSSO: true}, nil + } else if h.isMFARequired(email, organizationId) { + return &Credentials{Username: email, IsMFA: true}, nil } password := h.promptForPassword() return &Credentials{Username: email, Password: password}, nil @@ -278,6 +285,25 @@ func (h *LoginCredentialsManagerImpl) promptForPassword() string { return f.Responses[passwordField].(string) } +func (h *LoginCredentialsManagerImpl) isMFARequired(email, organizationId string) bool { + if h.client == nil { + return false + } + + auth0ClientId := sso.GetAuth0CCloudClientIdFromBaseUrl(h.client.BaseURL) + log.CliLogger.Tracef("h.client.BaseURL: %s", h.client.BaseURL) + log.CliLogger.Tracef("auth0ClientId: %s", auth0ClientId) + req := &ccloudv1.GetLoginRealmRequest{ + Email: email, + ClientId: auth0ClientId, + OrgResourceId: organizationId, + } + res, err := h.client.User.LoginRealm(req) + // Fine to ignore non-nil err for this request: e.g. what if this fails due to invalid/malicious + // email, we want to silently continue and give the illusion of password prompt. + return err == nil && res.GetMfaRequired() +} + func (h *LoginCredentialsManagerImpl) isSSOUser(email, organizationId string) bool { if h.client == nil { return false diff --git a/pkg/auth/login_credentials_manager_test.go b/pkg/auth/login_credentials_manager_test.go index 8055c3bb61..e287fb7343 100644 --- a/pkg/auth/login_credentials_manager_test.go +++ b/pkg/auth/login_credentials_manager_test.go @@ -72,9 +72,12 @@ func (suite *LoginCredentialsManagerTestSuite) SetupSuite() { User: &ccloudv1mock.UserInterface{ LoginRealmFunc: func(req *ccloudv1.GetLoginRealmRequest) (*ccloudv1.GetLoginRealmReply, error) { if req.Email == "test+sso@confluent.io" { - return &ccloudv1.GetLoginRealmReply{IsSso: true, Realm: "ccloud-local"}, nil + return &ccloudv1.GetLoginRealmReply{IsSso: true, MfaRequired: false, Realm: "ccloud-local"}, nil } - return &ccloudv1.GetLoginRealmReply{IsSso: false, Realm: "ccloud-local"}, nil + if req.Email == "test+mfa@confluent.io" { + return &ccloudv1.GetLoginRealmReply{MfaRequired: true, Realm: "ccloud-local"}, nil + } + return &ccloudv1.GetLoginRealmReply{IsSso: false, MfaRequired: false, Realm: "ccloud-local"}, nil }, }, } @@ -107,6 +110,11 @@ func (suite *LoginCredentialsManagerTestSuite) TestGetCCloudCredentialsFromEnvVa suite.require.NoError(err) suite.compareCredentials(&Credentials{Username: "test+sso@confluent.io", IsSSO: true, Password: ""}, creds) + suite.require.NoError(os.Setenv(ConfluentCloudEmail, "test+mfa@confluent.io")) + creds, err = suite.loginCredentialsManager.GetCloudCredentialsFromEnvVar("")() + suite.require.NoError(err) + suite.compareCredentials(&Credentials{Username: "test+mfa@confluent.io", IsMFA: true, Password: ""}, creds) + suite.setCCEnvVars() creds, err = suite.loginCredentialsManager.GetCloudCredentialsFromEnvVar("")() suite.require.NoError(err) diff --git a/pkg/auth/mfa/auth_mfa_flow.go b/pkg/auth/mfa/auth_mfa_flow.go new file mode 100644 index 0000000000..3ce7c03108 --- /dev/null +++ b/pkg/auth/mfa/auth_mfa_flow.go @@ -0,0 +1,68 @@ +package mfa + +import ( + "fmt" + "strings" + "time" + + "github.com/pkg/browser" + + "github.com/confluentinc/cli/v4/pkg/auth/sso" +) + +func Login(authURL, email, connectionName string) (string, string, error) { + state, err := newState(authURL, email) + if err != nil { + return "", "", err + } + + isOkta := sso.IsOkta(authURL) + + server := newServer(state) + if err := server.startServer(); err != nil { + return "", "", fmt.Errorf("unable to start HTTP server: %w", err) + } + // Get authorization code for making subsequent token request + url := state.getAuthorizationCodeUrl(isOkta, connectionName) + if err := browser.OpenURL(url); err != nil { + return "", "", fmt.Errorf("unable to open web browser for authorization: %w", err) + } + + if err := server.awaitAuthorizationCode(300 * time.Second); err != nil { + return "", "", err + } + + // Exchange authorization code for OAuth token from provider + if err := state.getOAuthToken(); err != nil { + return "", "", err + } + + return state.MFAProviderIDToken, state.MFAProviderRefreshToken, nil +} + +func RefreshTokens(authURL, refreshToken, email string) (string, string, error) { + state, err := newState(authURL, email) + if err != nil { + return "", "", err + } + state.MFAProviderRefreshToken = refreshToken + + if err := state.refreshOAuthToken(); err != nil { + return "", "", err + } + + return state.MFAProviderIDToken, state.MFAProviderRefreshToken, nil +} +func (s *authState) refreshOAuthToken() error { + payload := strings.NewReader("grant_type=refresh_token" + + "&client_id=" + s.MFAProviderClientID + + "&refresh_token=" + s.MFAProviderRefreshToken + + "&redirect_uri=" + s.MFAProviderCallbackUrl) + + data, err := s.getOAuthTokenResponse(payload) + if err != nil { + return err + } + + return s.saveOAuthTokenResponse(data) +} diff --git a/pkg/auth/mfa/auth_server.go b/pkg/auth/mfa/auth_server.go new file mode 100644 index 0000000000..9419cc9518 --- /dev/null +++ b/pkg/auth/mfa/auth_server.go @@ -0,0 +1,108 @@ +package mfa + +import ( + "context" + _ "embed" + "fmt" + "net" + "net/http" + "time" + + "github.com/confluentinc/cli/v4/pkg/errors" + "github.com/confluentinc/cli/v4/pkg/output" +) + +//go:embed mfa_callback.html +var mfaCallbackHTML string + +// Ideally we would randomize this value, but Auth0 requires that we hardcode a single port. +const port = 26635 + +/* +authServer is an HTTP server embedded in the CLI to serve callback requests for SSO logins. +The server runs in a goroutine / in the background. +*/ +type authServer struct { + server *http.Server + wait chan bool + bgErr error + State *authState +} + +func newServer(state *authState) *authServer { + return &authServer{ + wait: make(chan bool), + State: state, + } +} + +// Start begins the server including attempting to bind the desired TCP port +func (s *authServer) startServer() error { + mux := http.NewServeMux() + mux.HandleFunc("/cli_callback", s.callbackHandler) + + // A process can intercept the Auth0 callback by listening to 0.0.0.0:. Verify that this is not the case. + lis, err := net.ListenTCP("tcp", &net.TCPAddr{Port: port}) + if err != nil { + return err + } + _ = lis.Close() + + lis, err = net.ListenTCP("tcp4", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) + if err != nil { + return err + } + + s.server = &http.Server{Handler: mux} + + go func() { + serverErr := s.server.Serve(lis) + // Serve returns ErrServerClosed when the server is gracefully, intentionally closed: + // https://go.googlesource.com/go/+/master/src/net/http/server.go#2854 + // So don't surface that error to the user. + if serverErr != nil && serverErr.Error() != "http: Server closed" { + output.ErrPrintf(false, "CLI HTTP auth server encountered error while running: %v\n", serverErr.Error()) + } + }() + + return nil +} + +// GetAuthorizationCode takes the code verifier/challenge and gets an authorization code from the SSO provider +func (s *authServer) awaitAuthorizationCode(timeout time.Duration) error { + // Wait until flow is finished / callback is called (or timeout...) + go func() { + time.Sleep(timeout) + s.bgErr = errors.NewErrorWithSuggestions(errors.BrowserAuthTimedOutErrorMsg, errors.BrowserAuthTimedOutSuggestions) + s.server.Close() + s.wait <- true + }() + <-s.wait + + defer func() { + if err := s.server.Shutdown(context.Background()); err != nil { + output.ErrPrintf(false, "CLI HTTP auth server encountered error while shutting down: %v\n", err) + } + }() + + return s.bgErr +} + +// callbackHandler serves the route /callback +func (s *authServer) callbackHandler(w http.ResponseWriter, r *http.Request) { + states, ok := r.URL.Query()["state"] + if !(ok && states[0] == s.State.MFAProviderState) { + s.bgErr = fmt.Errorf("authentication callback URL either did not contain a state parameter in query string, or the state parameter was invalid; login will fail") + } + + fmt.Fprintln(w, mfaCallbackHTML) + + codes, ok := r.URL.Query()["code"] + if ok { + s.State.MFAProviderAuthenticationCode = codes[0] + } else { + s.bgErr = fmt.Errorf("authentication callback URL did not contain code parameter in query string; login will fail") + } + + s.wait <- true +} diff --git a/pkg/auth/mfa/auth_server_test.go b/pkg/auth/mfa/auth_server_test.go new file mode 100644 index 0000000000..94abf2657e --- /dev/null +++ b/pkg/auth/mfa/auth_server_test.go @@ -0,0 +1,102 @@ +package mfa + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/confluentinc/cli/v4/pkg/auth/sso" + "github.com/confluentinc/cli/v4/pkg/errors" +) + +func TestServerTimeout(t *testing.T) { + //MFA Server + state, err := newState("https://devel.cpdev.cloud", "test+mfa@confluent.io") + require.NoError(t, err) + server := newServer(state) + + require.NoError(t, server.startServer()) + + err = server.awaitAuthorizationCode(1 * time.Second) + require.Error(t, err) + require.Equal(t, err.Error(), errors.BrowserAuthTimedOutErrorMsg) + errors.VerifyErrorAndSuggestions(require.New(t), err, errors.BrowserAuthTimedOutErrorMsg, errors.BrowserAuthTimedOutSuggestions) + + //SSO Server + stateSSO, err := sso.NewState("https://devel.cpdev.cloud", false) + require.NoError(t, err) + serverSSO := sso.NewServer(stateSSO) + + require.NoError(t, serverSSO.StartServer()) + + err = serverSSO.AwaitAuthorizationCode(1 * time.Second) + require.Error(t, err) + require.Equal(t, err.Error(), errors.BrowserAuthTimedOutErrorMsg) + errors.VerifyErrorAndSuggestions(require.New(t), err, errors.BrowserAuthTimedOutErrorMsg, errors.BrowserAuthTimedOutSuggestions) +} + +func TestCallback(t *testing.T) { + //MFA Callback + state, err := newState("https://devel.cpdev.cloud", "test+mfa@confluent.io") + require.NoError(t, err) + server := newServer(state) + + require.NoError(t, server.startServer()) + + state.MFAProviderCallbackUrl = "http://127.0.0.1:26635/cli_callback" + url := state.MFAProviderCallbackUrl + mockCode := "uhlU7Fvq5NwLwBwk" + mockUri := url + "?code=" + mockCode + "&state=" + state.MFAProviderState + + ch := make(chan bool) + go func() { + <-ch + // send mock request to server's callback endpoint + req, err := http.NewRequest(http.MethodGet, mockUri, nil) + require.NoError(t, err) + _, err = http.DefaultClient.Do(req) + require.NoError(t, err) + }() + + go func() { + // trigger the callback function after waiting a sec + time.Sleep(500) + close(ch) + }() + authCodeError := server.awaitAuthorizationCode(3 * time.Second) + require.NoError(t, authCodeError) + require.Equal(t, state.MFAProviderAuthenticationCode, "uhlU7Fvq5NwLwBwk") + + //SSO Callback + stateSSO, err := sso.NewState("https://devel.cpdev.cloud", false) + require.NoError(t, err) + serverSSO := sso.NewServer(stateSSO) + + require.NoError(t, serverSSO.StartServer()) + + stateSSO.SSOProviderCallbackUrl = "http://127.0.0.1:26635/cli_callback" + urlSSO := stateSSO.SSOProviderCallbackUrl + mockCodeSSO := "uhlU7Fvq5NwLwBwk" + mockUriSSO := urlSSO + "?code=" + mockCodeSSO + "&state=" + stateSSO.SSOProviderState + + c := make(chan bool) + go func() { + <-c + // send mock request to server's callback endpoint + req, err := http.NewRequest(http.MethodGet, mockUriSSO, nil) + require.NoError(t, err) + _, err = http.DefaultClient.Do(req) + require.NoError(t, err) + }() + + go func() { + // trigger the callback function after waiting a sec + time.Sleep(500) + close(c) + }() + authCodeErrorSSO := serverSSO.AwaitAuthorizationCode(3 * time.Second) + require.NoError(t, authCodeErrorSSO) + require.Equal(t, stateSSO.SSOProviderAuthenticationCode, "uhlU7Fvq5NwLwBwk") +} diff --git a/pkg/auth/mfa/auth_state.go b/pkg/auth/mfa/auth_state.go new file mode 100644 index 0000000000..5a5617de3f --- /dev/null +++ b/pkg/auth/mfa/auth_state.go @@ -0,0 +1,163 @@ +package mfa + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/confluentinc/cli/v4/pkg/auth/sso" + "github.com/confluentinc/cli/v4/pkg/errors" + "github.com/confluentinc/cli/v4/pkg/log" +) + +var mfaProviderCallbackLocalURL = fmt.Sprintf("http://127.0.0.1:%d", port) + sso.SsoProviderCallbackEndpoint + +type authState struct { + CodeVerifier string + CodeChallenge string + MFAProviderAuthenticationCode string + MFAProviderIDToken string + MFAProviderRefreshToken string + MFAProviderState string + Email string + MFAProviderCallbackUrl string + MFAProviderHost string + MFAProviderClientID string + MFAProviderScope string + MFAProviderIdentifier string +} + +func newState(authUrl, email string) (*authState, error) { + if authUrl == "" { + authUrl = "https://confluent.cloud" + } + + env := sso.GetCCloudEnvFromBaseUrl(authUrl) + + state := &authState{ + MFAProviderHost: "https://" + sso.SsoConfigs[env].SsoProviderDomain, + MFAProviderCallbackUrl: mfaProviderCallbackLocalURL, + MFAProviderClientID: sso.GetAuth0CCloudClientIdFromBaseUrl(authUrl), + Email: email, + MFAProviderIdentifier: sso.SsoConfigs[env].SsoProviderIdentifier, + MFAProviderScope: sso.SsoConfigs[env].SsoProviderScope, + } + if err := state.generateCodes(); err != nil { + return nil, err + } + return state, nil +} + +func (s *authState) generateCodes() error { + randomBytes := make([]byte, 32) + + if _, err := rand.Read(randomBytes); err != nil { + return fmt.Errorf("unable to generate random bytes for SSO provider state: %w", err) + } + + s.MFAProviderState = base64.RawURLEncoding.EncodeToString(randomBytes) + + if _, err := rand.Read(randomBytes); err != nil { + return fmt.Errorf("unable to generate random bytes for code verifier: %w", err) + } + + s.CodeVerifier = base64.RawURLEncoding.EncodeToString(randomBytes) + + hash := sha256.New() + if _, err := hash.Write([]byte(s.CodeVerifier)); err != nil { + return fmt.Errorf("unable to compute hash for code challenge: %w", err) + } + s.CodeChallenge = base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) + + return nil +} + +func (s *authState) getAuthorizationCodeUrl(isOkta bool, mfaProviderConnectionName string) string { + urlMFA := s.MFAProviderHost + "/authorize?challenge_mfa=true" + + "&response_type=code" + + "&email=" + encodeEmail(s.Email) + + "&from_cli=true&mfa_from_cli=true" + + "&code_challenge=" + s.CodeChallenge + + "&code_challenge_method=S256" + + "&client_id=" + s.MFAProviderClientID + + "&redirect_uri=" + s.MFAProviderCallbackUrl + + "&scope=" + s.MFAProviderScope + + "&state=" + s.MFAProviderState + + if s.MFAProviderIdentifier != "" { + urlMFA += "&audience=" + s.MFAProviderIdentifier + } + + if isOkta { + urlMFA += "&idp=" + mfaProviderConnectionName + } else { + urlMFA += "&connection=" + mfaProviderConnectionName + } + + return urlMFA +} + +func (s *authState) saveOAuthTokenResponse(data map[string]any) error { + if token, ok := data["id_token"]; ok { + s.MFAProviderIDToken = token.(string) + } else { + return fmt.Errorf("incorrect token added. Please try to login again") + } + + if token, ok := data["refresh_token"]; ok { + s.MFAProviderRefreshToken = token.(string) + } else { + return fmt.Errorf(errors.FmtMissingOauthFieldErrorMsg, "refresh_token") + } + + return nil +} + +func (s *authState) getOAuthToken() error { + payload := strings.NewReader("grant_type=authorization_code&from_cli=true&mfa_from_cli=true" + + "&client_id=" + s.MFAProviderClientID + + "&code_verifier=" + s.CodeVerifier + + "&code=" + s.MFAProviderAuthenticationCode + + "&redirect_uri=" + s.MFAProviderCallbackUrl) + + data, err := s.getOAuthTokenResponse(payload) + if err != nil { + return err + } + + return s.saveOAuthTokenResponse(data) +} + +func (s *authState) getOAuthTokenResponse(payload *strings.Reader) (map[string]any, error) { + url := s.MFAProviderHost + "/token" + log.CliLogger.Debugf("OAuth token request URL: %s", url) + log.CliLogger.Debug("OAuth token request payload: ", payload) + req, err := http.NewRequest(http.MethodPost, url, payload) + if err != nil { + return nil, fmt.Errorf("failed to construct oauth token re, errquest: %w", err) + } + req.Header.Add("content-type", "application/x-www-form-urlencoded") + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to get oauth token: %w", err) + } + defer res.Body.Close() + errorResponseBody, _ := io.ReadAll(res.Body) + var data map[string]any + if err := json.Unmarshal(errorResponseBody, &data); err != nil { + log.CliLogger.Debugf("Failed oauth token response body: %s", errorResponseBody) + return nil, fmt.Errorf("failed to unmarshal response body in oauth token request: %w", err) + } + return data, nil +} + +func encodeEmail(email string) string { + encodedEmail := url.QueryEscape(email) + return encodedEmail +} diff --git a/pkg/auth/mfa/auth_state_test.go b/pkg/auth/mfa/auth_state_test.go new file mode 100644 index 0000000000..ced7458bdd --- /dev/null +++ b/pkg/auth/mfa/auth_state_test.go @@ -0,0 +1,159 @@ +package mfa + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewStateDev(t *testing.T) { + state, err := newState("https://devel.cpdev.cloud/", "test+mfa@conluent.io") + require.NoError(t, err) + // randomly generated + require.True(t, len(state.CodeVerifier) > 10) + require.True(t, len(state.CodeChallenge) > 10) + require.True(t, len(state.MFAProviderState) > 10) + // make sure we didn't so something dumb generating the hashes + require.True(t, + (state.CodeVerifier != state.CodeChallenge) && + (state.CodeVerifier != state.MFAProviderState) && + (state.CodeChallenge != state.MFAProviderState)) + // dev configs + require.Equal(t, "https://login.confluent-dev.io/oauth", state.MFAProviderHost) + require.Equal(t, "sPhOuMMVRSFFR7HfB606KLxf1RAU4SXg", state.MFAProviderClientID) + require.Equal(t, "http://127.0.0.1:26635/cli_callback", state.MFAProviderCallbackUrl) + require.Equal(t, "https://confluent-dev.auth0.com/api/v2/", state.MFAProviderIdentifier) + require.Empty(t, state.MFAProviderAuthenticationCode) + require.Empty(t, state.MFAProviderIDToken) + + // check stag configs + stateStag, err := newState("https://stag.cpdev.cloud/", "test+mfa@conluent.io") + require.NoError(t, err) + require.Equal(t, "https://login-stag.confluent-dev.io/oauth", stateStag.MFAProviderHost) + require.Equal(t, "8RxQmZEYtEDah4MTIIzl4hGGeFwdJS6w", stateStag.MFAProviderClientID) + require.Equal(t, "http://127.0.0.1:26635/cli_callback", stateStag.MFAProviderCallbackUrl) + require.Equal(t, "https://confluent-stag.auth0.com/api/v2/", stateStag.MFAProviderIdentifier) + require.Empty(t, state.MFAProviderAuthenticationCode) + require.Empty(t, state.MFAProviderIDToken) +} + +func TestNewStateInvalidUrl(t *testing.T) { + state, err := newState("Invalid url", "xyz.com") + require.NoError(t, err) + require.NotNil(t, state) +} + +func TestGetAuthorizationUrl(t *testing.T) { + state, err := newState("https://devel.cpdev.cloud", "test+mfa@confluent.io") + require.NoError(t, err) + + // test get auth code url + authCodeUrlDevel := state.getAuthorizationCodeUrl(false, "connection1") + expectedUri := "/authorize?challenge_mfa=true" + + "&response_type=code" + + "&email=" + encodeEmail(state.Email) + + "&from_cli=true&mfa_from_cli=true" + + "&code_challenge=" + state.CodeChallenge + + "&code_challenge_method=S256" + + "&client_id=" + state.MFAProviderClientID + + "&redirect_uri=" + state.MFAProviderCallbackUrl + + "&scope=" + state.MFAProviderScope + + "&state=" + state.MFAProviderState + + "&audience=" + state.MFAProviderIdentifier + + "&connection=connection1" + expectedUrl := state.MFAProviderHost + expectedUri + require.Equal(t, authCodeUrlDevel, expectedUrl) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Test request parameters + require.Equal(t, req.URL.String(), expectedUri) + // Send response to be tested + _, e := rw.Write([]byte(`OK`)) + require.NoError(t, e) + })) + defer server.Close() + require.NotEmpty(t, server.URL) +} + +func TestGetOAuthToken(t *testing.T) { + mockRefreshToken := "foo" + + state, err := newState("https://devel.cpdev.cloud", "test+mfa@confluent.io") + require.NoError(t, err) + + expectedUri := "/token" + expectedPayload := "grant_type=authorization_code&from_cli=true&mfa_from_cli=true" + + "&client_id=" + state.MFAProviderClientID + + "&code_verifier=" + state.CodeVerifier + + "&code=" + state.MFAProviderAuthenticationCode + + "&redirect_uri=" + state.MFAProviderCallbackUrl + + mockIDToken := "foobar" + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Test request parameters + require.Equal(t, req.URL.String(), expectedUri) + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.True(t, len(body) > 0) + require.Equal(t, expectedPayload, string(body)) + + // mock response + _, err = rw.Write([]byte(fmt.Sprintf(`{"id_token": "%s", "refresh_token": "%s"}`, mockIDToken, mockRefreshToken))) + require.NoError(t, err) + })) + defer server.Close() + serverPort := strings.Split(server.URL, ":")[2] + + // mock auth0 endpoint with local test server + state.MFAProviderHost = "http://127.0.0.1:" + serverPort + + err = state.getOAuthToken() + require.NoError(t, err) + + require.Equal(t, mockIDToken, state.MFAProviderIDToken) +} + +func TestRefreshOAuthToken(t *testing.T) { + mockRefreshToken1 := "foo" + mockRefreshToken2 := "bar" + + state, err := newState("https://devel.cpdev.cloud", "test+mfa@confluent.io") + require.NoError(t, err) + state.MFAProviderRefreshToken = mockRefreshToken1 + + expectedUri := "/token" + expectedPayload := "grant_type=refresh_token" + + "&client_id=" + state.MFAProviderClientID + + "&refresh_token=" + state.MFAProviderRefreshToken + + "&redirect_uri=" + state.MFAProviderCallbackUrl + + mockIDToken := "foobar" + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Test request parameters + require.Equal(t, req.URL.String(), expectedUri) + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.True(t, len(body) > 0) + require.Equal(t, expectedPayload, string(body)) + + // mock response + _, err = rw.Write([]byte(fmt.Sprintf(`{"id_token": "%s", "refresh_token": "%s"}`, mockIDToken, mockRefreshToken2))) + require.NoError(t, err) + })) + defer server.Close() + serverPort := strings.Split(server.URL, ":")[2] + + // mock auth0 endpoint with local test server + state.MFAProviderHost = "http://127.0.0.1:" + serverPort + + err = state.refreshOAuthToken() + require.NoError(t, err) + + require.Equal(t, mockIDToken, state.MFAProviderIDToken) + require.Equal(t, mockRefreshToken2, state.MFAProviderRefreshToken) +} diff --git a/pkg/auth/mfa/mfa_callback.html b/pkg/auth/mfa/mfa_callback.html new file mode 100644 index 0000000000..f5d78b17a3 --- /dev/null +++ b/pkg/auth/mfa/mfa_callback.html @@ -0,0 +1,90 @@ + + + + + Confluent CLI + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/auth/sso/auth_server.go b/pkg/auth/sso/auth_server.go index 02383f1ff0..e74c628395 100644 --- a/pkg/auth/sso/auth_server.go +++ b/pkg/auth/sso/auth_server.go @@ -29,7 +29,7 @@ type authServer struct { State *authState } -func newServer(state *authState) *authServer { +func NewServer(state *authState) *authServer { return &authServer{ wait: make(chan bool), State: state, @@ -37,7 +37,7 @@ func newServer(state *authState) *authServer { } // Start begins the server including attempting to bind the desired TCP port -func (s *authServer) startServer() error { +func (s *authServer) StartServer() error { mux := http.NewServeMux() mux.HandleFunc("/cli_callback", s.callbackHandler) @@ -69,7 +69,7 @@ func (s *authServer) startServer() error { } // GetAuthorizationCode takes the code verifier/challenge and gets an authorization code from the SSO provider -func (s *authServer) awaitAuthorizationCode(timeout time.Duration) error { +func (s *authServer) AwaitAuthorizationCode(timeout time.Duration) error { // Wait until flow is finished / callback is called (or timeout...) go func() { time.Sleep(timeout) diff --git a/pkg/auth/sso/auth_server_test.go b/pkg/auth/sso/auth_server_test.go deleted file mode 100644 index e9eb1abe3b..0000000000 --- a/pkg/auth/sso/auth_server_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package sso - -import ( - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/confluentinc/cli/v4/pkg/errors" -) - -func TestServerTimeout(t *testing.T) { - state, err := newState("https://devel.cpdev.cloud", false) - require.NoError(t, err) - server := newServer(state) - - require.NoError(t, server.startServer()) - - err = server.awaitAuthorizationCode(1 * time.Second) - require.Error(t, err) - require.Equal(t, err.Error(), errors.BrowserAuthTimedOutErrorMsg) - errors.VerifyErrorAndSuggestions(require.New(t), err, errors.BrowserAuthTimedOutErrorMsg, errors.BrowserAuthTimedOutSuggestions) -} - -func TestCallback(t *testing.T) { - state, err := newState("https://devel.cpdev.cloud", false) - require.NoError(t, err) - server := newServer(state) - - require.NoError(t, server.startServer()) - - state.SSOProviderCallbackUrl = "http://127.0.0.1:26635/cli_callback" - url := state.SSOProviderCallbackUrl - mockCode := "uhlU7Fvq5NwLwBwk" - mockUri := url + "?code=" + mockCode + "&state=" + state.SSOProviderState - - ch := make(chan bool) - go func() { - <-ch - // send mock request to server's callback endpoint - req, err := http.NewRequest(http.MethodGet, mockUri, nil) - require.NoError(t, err) - _, err = http.DefaultClient.Do(req) - require.NoError(t, err) - }() - - go func() { - // trigger the callback function after waiting a sec - time.Sleep(500) - close(ch) - }() - authCodeError := server.awaitAuthorizationCode(3 * time.Second) - require.NoError(t, authCodeError) - require.Equal(t, state.SSOProviderAuthenticationCode, "uhlU7Fvq5NwLwBwk") -} diff --git a/pkg/auth/sso/auth_sso_flow.go b/pkg/auth/sso/auth_sso_flow.go index 9036414231..b6316b3927 100644 --- a/pkg/auth/sso/auth_sso_flow.go +++ b/pkg/auth/sso/auth_sso_flow.go @@ -13,7 +13,7 @@ import ( ) func Login(authURL string, noBrowser bool, connectionName string) (string, string, error) { - state, err := newState(authURL, noBrowser) + state, err := NewState(authURL, noBrowser) if err != nil { return "", "", err } @@ -46,8 +46,8 @@ func Login(authURL string, noBrowser bool, connectionName string) (string, strin } else { // we need to start a background HTTP server to support the authorization code flow with PKCE // described at https://auth0.com/docs/flows/guides/auth-code-pkce/call-api-auth-code-pkce - server := newServer(state) - if err := server.startServer(); err != nil { + server := NewServer(state) + if err := server.StartServer(); err != nil { return "", "", fmt.Errorf("unable to start HTTP server: %w", err) } @@ -57,7 +57,7 @@ func Login(authURL string, noBrowser bool, connectionName string) (string, strin return "", "", fmt.Errorf("unable to open web browser for authorization: %w", err) } - if err := server.awaitAuthorizationCode(30 * time.Second); err != nil { + if err := server.AwaitAuthorizationCode(30 * time.Second); err != nil { return "", "", err } } @@ -71,7 +71,7 @@ func Login(authURL string, noBrowser bool, connectionName string) (string, strin } func RefreshTokens(authURL, refreshToken string) (string, string, error) { - state, err := newState(authURL, false) + state, err := NewState(authURL, false) if err != nil { return "", "", err } diff --git a/pkg/auth/sso/auth_state.go b/pkg/auth/sso/auth_state.go index 24349f0ab7..40d1d15339 100644 --- a/pkg/auth/sso/auth_state.go +++ b/pkg/auth/sso/auth_state.go @@ -15,49 +15,49 @@ import ( ) var ( - ssoProviderCallbackEndpoint = "/cli_callback" - ssoProviderCallbackLocalURL = fmt.Sprintf("http://127.0.0.1:%d", port) + ssoProviderCallbackEndpoint + SsoProviderCallbackEndpoint = "/cli_callback" + ssoProviderCallbackLocalURL = fmt.Sprintf("http://127.0.0.1:%d", port) + SsoProviderCallbackEndpoint - ssoConfigs = map[string]ssoConfig{ + SsoConfigs = map[string]ssoConfig{ "devel": { - ssoProviderDomain: "login.confluent-dev.io/oauth", - ssoProviderIdentifier: "https://confluent-dev.auth0.com/api/v2/", - ssoProviderScope: "email%20openid%20offline_access", + SsoProviderDomain: "login.confluent-dev.io/oauth", + SsoProviderIdentifier: "https://confluent-dev.auth0.com/api/v2/", + SsoProviderScope: "email%20openid%20offline_access", }, "devel-us-gov": { - ssoProviderDomain: "confluent-devel-us-gov.oktapreview.com/oauth2/v1", - ssoProviderScope: "openid+profile+email+offline_access", + SsoProviderDomain: "confluent-devel-us-gov.oktapreview.com/oauth2/v1", + SsoProviderScope: "openid+profile+email+offline_access", }, "infra-us-gov": { - ssoProviderDomain: "confluent-infra-us-gov.okta.com/oauth2/v1", - ssoProviderScope: "openid+profile+email+offline_access", + SsoProviderDomain: "confluent-infra-us-gov.okta.com/oauth2/v1", + SsoProviderScope: "openid+profile+email+offline_access", }, "prod": { - ssoProviderDomain: "login.confluent.io/oauth", - ssoProviderIdentifier: "https://confluent.auth0.com/api/v2/", - ssoProviderScope: "email%20openid%20offline_access", + SsoProviderDomain: "login.confluent.io/oauth", + SsoProviderIdentifier: "https://confluent.auth0.com/api/v2/", + SsoProviderScope: "email%20openid%20offline_access", }, "prod-us-gov": { - ssoProviderDomain: "confluent-prod-us-gov.okta.com/oauth2/v1", - ssoProviderScope: "openid+profile+email+offline_access", + SsoProviderDomain: "confluent-prod-us-gov.okta.com/oauth2/v1", + SsoProviderScope: "openid+profile+email+offline_access", }, "stag": { - ssoProviderDomain: "login-stag.confluent-dev.io/oauth", - ssoProviderIdentifier: "https://confluent-stag.auth0.com/api/v2/", - ssoProviderScope: "email%20openid%20offline_access", + SsoProviderDomain: "login-stag.confluent-dev.io/oauth", + SsoProviderIdentifier: "https://confluent-stag.auth0.com/api/v2/", + SsoProviderScope: "email%20openid%20offline_access", }, "test": { - ssoProviderDomain: "test.com/oauth", - ssoProviderIdentifier: "https://test.auth0.com/api/v2/", - ssoProviderScope: "email%20openid%20offline_access", + SsoProviderDomain: "test.com/oauth", + SsoProviderIdentifier: "https://test.auth0.com/api/v2/", + SsoProviderScope: "email%20openid%20offline_access", }, } ) type ssoConfig struct { - ssoProviderDomain string - ssoProviderIdentifier string - ssoProviderScope string + SsoProviderDomain string + SsoProviderIdentifier string + SsoProviderScope string } /* @@ -81,7 +81,7 @@ type authState struct { // InitState generates various auth0 related codes and hashes // and tweaks certain variables for internal development and testing of the CLIs // auth0 server / SSO integration. -func newState(authUrl string, noBrowser bool) (*authState, error) { +func NewState(authUrl string, noBrowser bool) (*authState, error) { if authUrl == "" { authUrl = "https://confluent.cloud" } @@ -89,11 +89,11 @@ func newState(authUrl string, noBrowser bool) (*authState, error) { env := GetCCloudEnvFromBaseUrl(authUrl) state := &authState{ - SSOProviderCallbackUrl: strings.TrimSuffix(authUrl, "/") + ssoProviderCallbackEndpoint, - SSOProviderHost: "https://" + ssoConfigs[env].ssoProviderDomain, + SSOProviderCallbackUrl: strings.TrimSuffix(authUrl, "/") + SsoProviderCallbackEndpoint, + SSOProviderHost: "https://" + SsoConfigs[env].SsoProviderDomain, SSOProviderClientID: GetAuth0CCloudClientIdFromBaseUrl(authUrl), - SSOProviderIdentifier: ssoConfigs[env].ssoProviderIdentifier, - SSOProviderScope: ssoConfigs[env].ssoProviderScope, + SSOProviderIdentifier: SsoConfigs[env].SsoProviderIdentifier, + SSOProviderScope: SsoConfigs[env].SsoProviderScope, } if !noBrowser { diff --git a/pkg/auth/sso/auth_state_test.go b/pkg/auth/sso/auth_state_test.go index 8d2835e82f..8650358819 100644 --- a/pkg/auth/sso/auth_state_test.go +++ b/pkg/auth/sso/auth_state_test.go @@ -12,7 +12,7 @@ import ( ) func TestNewStateDev(t *testing.T) { - state, err := newState("https://devel.cpdev.cloud/", false) + state, err := NewState("https://devel.cpdev.cloud/", false) require.NoError(t, err) // randomly generated require.True(t, len(state.CodeVerifier) > 10) @@ -32,7 +32,7 @@ func TestNewStateDev(t *testing.T) { require.Empty(t, state.SSOProviderIDToken) // check stag configs - stateStag, err := newState("https://stag.cpdev.cloud/", false) + stateStag, err := NewState("https://stag.cpdev.cloud/", false) require.NoError(t, err) require.Equal(t, "https://login-stag.confluent-dev.io/oauth", stateStag.SSOProviderHost) require.Equal(t, "8RxQmZEYtEDah4MTIIzl4hGGeFwdJS6w", stateStag.SSOProviderClientID) @@ -43,7 +43,7 @@ func TestNewStateDev(t *testing.T) { } func TestNewStateDevNoBrowser(t *testing.T) { - state, err := newState("https://devel.cpdev.cloud", true) + state, err := NewState("https://devel.cpdev.cloud", true) require.NoError(t, err) // randomly generated require.True(t, len(state.CodeVerifier) > 10) @@ -64,7 +64,7 @@ func TestNewStateDevNoBrowser(t *testing.T) { require.Empty(t, state.SSOProviderIDToken) // check stag configs - stateStag, err := newState("https://stag.cpdev.cloud", true) + stateStag, err := NewState("https://stag.cpdev.cloud", true) require.NoError(t, err) require.Equal(t, "https://login-stag.confluent-dev.io/oauth", stateStag.SSOProviderHost) require.Equal(t, "8RxQmZEYtEDah4MTIIzl4hGGeFwdJS6w", stateStag.SSOProviderClientID) @@ -75,7 +75,7 @@ func TestNewStateDevNoBrowser(t *testing.T) { } func TestNewStateProd(t *testing.T) { - state, err := newState("https://confluent.cloud", false) + state, err := NewState("https://confluent.cloud", false) require.NoError(t, err) // randomly generated require.True(t, len(state.CodeVerifier) > 10) @@ -96,7 +96,7 @@ func TestNewStateProd(t *testing.T) { func TestNewStateProdNoBrowser(t *testing.T) { for _, authURL := range []string{"", "https://confluent.cloud"} { - state, err := newState(authURL, true) + state, err := NewState(authURL, true) require.NoError(t, err) // randomly generated @@ -120,13 +120,13 @@ func TestNewStateProdNoBrowser(t *testing.T) { } func TestNewStateInvalidUrl(t *testing.T) { - state, err := newState("Invalid url", true) + state, err := NewState("Invalid url", true) require.NoError(t, err) require.NotNil(t, state) } func TestGetAuthorizationUrl(t *testing.T) { - state, err := newState("https://devel.cpdev.cloud", false) + state, err := NewState("https://devel.cpdev.cloud", false) require.NoError(t, err) // test get auth code url @@ -158,7 +158,7 @@ func TestGetAuthorizationUrl(t *testing.T) { func TestGetOAuthToken(t *testing.T) { mockRefreshToken := "foo" - state, err := newState("https://devel.cpdev.cloud", false) + state, err := NewState("https://devel.cpdev.cloud", false) require.NoError(t, err) expectedUri := "/token" @@ -197,7 +197,7 @@ func TestRefreshOAuthToken(t *testing.T) { mockRefreshToken1 := "foo" mockRefreshToken2 := "bar" - state, err := newState("https://devel.cpdev.cloud", false) + state, err := NewState("https://devel.cpdev.cloud", false) require.NoError(t, err) state.SSOProviderRefreshToken = mockRefreshToken1 diff --git a/pkg/config/config.go b/pkg/config/config.go index b7ed299a91..53b046bc0b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -494,7 +494,7 @@ func (c *Config) FindContext(name string) (*Context, error) { return context, nil } -func (c *Config) AddContext(name, platformName, credentialName string, kafkaClusters map[string]*KafkaClusterConfig, kafka string, state *ContextState, organizationId, environmentId string) error { +func (c *Config) AddContext(name, platformName, credentialName string, kafkaClusters map[string]*KafkaClusterConfig, kafka string, state *ContextState, organizationId, environmentId string, isMFA bool) error { if _, ok := c.Contexts[name]; ok { return fmt.Errorf(errors.ContextAlreadyExistsErrorMsg, name) } @@ -509,7 +509,7 @@ func (c *Config) AddContext(name, platformName, credentialName string, kafkaClus return fmt.Errorf(`platform "%s" not found`, platformName) } - ctx, err := newContext(name, platform, credential, kafkaClusters, kafka, state, c, organizationId, environmentId) + ctx, err := newContext(name, platform, credential, kafkaClusters, kafka, state, c, organizationId, environmentId, isMFA) if err != nil { return err } @@ -561,7 +561,7 @@ func (c *Config) CreateContext(name, bootstrapURL, apiKey, apiSecret string) err } kafkaClusters := map[string]*KafkaClusterConfig{kafkaClusterCfg.ID: kafkaClusterCfg} - return c.AddContext(name, platform.Name, credential.Name, kafkaClusters, kafkaClusterCfg.ID, nil, "", "") + return c.AddContext(name, platform.Name, credential.Name, kafkaClusters, kafkaClusterCfg.ID, nil, "", "", false) } // UseContext sets the current context, if it exists. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 6eb3289035..008db0a774 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -588,6 +588,7 @@ func TestConfig_AddContext(t *testing.T) { state *ContextState Version *pversion.Version filename string + isMFA bool want *Config wantErr bool } @@ -602,6 +603,7 @@ func TestConfig_AddContext(t *testing.T) { kafka: context.KafkaClusterContext.ActiveKafkaCluster, state: context.State, filename: filename, + isMFA: context.IsMFA, } addValidContextTest := test @@ -620,7 +622,7 @@ func TestConfig_AddContext(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := test.config.AddContext(test.contextName, test.platformName, test.credentialName, test.kafkaClusters, test.kafka, test.state, MockOrgResourceId, test.currentEnvironment) + err := test.config.AddContext(test.contextName, test.platformName, test.credentialName, test.kafkaClusters, test.kafka, test.state, MockOrgResourceId, test.currentEnvironment, test.isMFA) if (err != nil) != test.wantErr { t.Errorf("AddContext() error = %v, wantErr %v", err, test.wantErr) } diff --git a/pkg/config/context.go b/pkg/config/context.go index 2d382dd4ea..d431351085 100644 --- a/pkg/config/context.go +++ b/pkg/config/context.go @@ -25,6 +25,7 @@ type Context struct { KafkaClusterContext *KafkaClusterContext `json:"kafka_cluster_context"` LastOrgId string `json:"last_org_id,omitempty"` FeatureFlags *FeatureFlags `json:"feature_flags,omitempty"` + IsMFA bool `json:"is_mfa,omitempty"` // Deprecated NetrcMachineName string `json:"netrc_machine_name,omitempty"` @@ -38,7 +39,7 @@ type Context struct { var noEnvError = "no environment found" -func newContext(name string, platform *Platform, credential *Credential, kafkaClusters map[string]*KafkaClusterConfig, kafka string, state *ContextState, config *Config, organizationId, environmentId string) (*Context, error) { +func newContext(name string, platform *Platform, credential *Credential, kafkaClusters map[string]*KafkaClusterConfig, kafka string, state *ContextState, config *Config, organizationId, environmentId string, isMFA bool) (*Context, error) { ctx := &Context{ Name: name, MachineName: name, @@ -51,6 +52,7 @@ func newContext(name string, platform *Platform, credential *Credential, kafkaCl State: state, Config: config, LastOrgId: organizationId, + IsMFA: isMFA, } ctx.KafkaClusterContext = NewKafkaClusterContext(ctx, kafka, kafkaClusters) if err := ctx.validate(); err != nil { diff --git a/pkg/config/mock.go b/pkg/config/mock.go index fa69420b7b..3c9f4885ec 100644 --- a/pkg/config/mock.go +++ b/pkg/config/mock.go @@ -107,7 +107,7 @@ func AuthenticatedConfigMock(params mockConfigParams) *Config { cfg := New() cfg.IsTest = true - ctx, err := newContext(params.contextName, platform, credential, kafkaClusters, kafkaCluster.ID, contextState, cfg, params.orgResourceId, params.envId) + ctx, err := newContext(params.contextName, platform, credential, kafkaClusters, kafkaCluster.ID, contextState, cfg, params.orgResourceId, params.envId, false) if err != nil { panic(err) }