From 0b117b52ea2e75db0b4d39eada7c7fba38801b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Hejman?= Date: Sun, 13 Oct 2024 16:27:52 +0200 Subject: [PATCH] Adding admin console auth (#836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding admin console auth. This might seem really funny at glance with the whole empty `NewElasticsearchAuthProvider()` which is essentially used only to add two routes, but in the future adding any oAuth provider should be trivial. In fact, I was able to create Google oAuth app and log in using my credentials. For now however, we have a nice login screen asking for our Elasticsearch credentials. The whole logic is really in `HandleElasticsearchLogin`. At this moment, **if Elasticsearch cluster allows unauthenticated access, Quesma disables admin console auth.** ![image (20)](https://github.com/user-attachments/assets/d21f4a1e-c42b-45e4-a8ce-bd3859fcfb5a) --------- Signed-off-by: Przemysław Hejman Co-authored-by: Jacek Migdal --- quesma/go.mod | 11 +++ quesma/go.sum | 35 +++++++ quesma/quesma/ui/asset/head.html | 51 ++++++++++ quesma/quesma/ui/console_routes.go | 135 +++++++++++++++++++++------ quesma/quesma/ui/es_auth_provider.go | 62 ++++++++++++ quesma/quesma/ui/html_utils.go | 2 +- quesma/quesma/ui/login.go | 66 +++++++++++++ 7 files changed, 331 insertions(+), 31 deletions(-) create mode 100644 quesma/quesma/ui/es_auth_provider.go create mode 100644 quesma/quesma/ui/login.go diff --git a/quesma/go.mod b/quesma/go.mod index fa7d89670..00c5f306e 100644 --- a/quesma/go.mod +++ b/quesma/go.mod @@ -28,13 +28,24 @@ require ( ) require ( + cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/gorilla/sessions v1.1.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/markbates/goth v1.80.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect + golang.org/x/oauth2 v0.17.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.33.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/quesma/go.sum b/quesma/go.sum index a49da8bb0..dfeaa28dc 100644 --- a/quesma/go.sum +++ b/quesma/go.sum @@ -1,3 +1,7 @@ +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= github.com/ClickHouse/clickhouse-go/v2 v2.29.0 h1:Dj1w59RssRyLgGHXtYaWU0eIM1pJsu9nGPi/btmvAqw= @@ -15,6 +19,7 @@ github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:h github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -34,6 +39,9 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -42,8 +50,14 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE= +github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -79,6 +93,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= +github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -106,6 +122,7 @@ github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXU github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -148,6 +165,7 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= @@ -158,20 +176,27 @@ go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZu golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -180,6 +205,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -188,20 +215,28 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/quesma/quesma/ui/asset/head.html b/quesma/quesma/ui/asset/head.html index ca9bfae76..070341ad7 100644 --- a/quesma/quesma/ui/asset/head.html +++ b/quesma/quesma/ui/asset/head.html @@ -266,6 +266,57 @@ outline: 0; } } + .login-screen { font-family: Courier, sans-serif; background-color: #2c2c2c; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } + + .login-form { + background-color: #3c3c3c; + padding: 20px; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + font-family: Courier, sans-serif; + } + + .login-form h2 { + margin-bottom: 20px; + color: #fff; + font-family: Courier, sans-serif; + } + + .login-form label { + display: block; + margin-bottom: 5px; + color: #ccc; + font-family: Courier, sans-serif; + } + + .login-form input[type="text"], + .login-form input[type="password"] { + width: calc(100% - 10px); + padding: 8px; + margin-bottom: 10px; + margin-right: 10px; + border: 1px solid #555; + border-radius: 4px; + background-color: #555; + color: #fff; + font-family: Courier, sans-serif; + } + + .login-form input[type="submit"] { + width: 100%; + padding: 10px; + background-color: #444; + border: none; + border-radius: 4px; + color: #fff; + font-size: 16px; + cursor: pointer; + font-family: Courier, sans-serif; + } + + .login-form input[type="submit"]:hover { + background-color: #333; + } .title-bar { background-color: #333; diff --git a/quesma/quesma/ui/console_routes.go b/quesma/quesma/ui/console_routes.go index b4eafca80..9ed7e49b9 100644 --- a/quesma/quesma/ui/console_routes.go +++ b/quesma/quesma/ui/console_routes.go @@ -1,5 +1,6 @@ // Copyright Quesma, licensed under the Elastic License 2.0. // SPDX-License-Identifier: Elastic-2.0 + package ui import ( @@ -7,6 +8,9 @@ import ( "encoding/json" "errors" "github.com/gorilla/mux" + "github.com/gorilla/sessions" + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" "net/http" "net/http/pprof" "quesma/logger" @@ -23,6 +27,26 @@ const ( //go:embed asset/* var uiFs embed.FS +const quesmaSessionName = "quesma-session" + +func authCallbackHandler(w http.ResponseWriter, r *http.Request) { + user, err := gothic.CompleteUserAuth(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + session, _ := store.Get(r, quesmaSessionName) + session.Values["userID"] = user.UserID + if err := session.Save(r, w); err != nil { + logger.Error().Msgf("Error saving session: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) +} + func (qmc *QuesmaManagementConsole) createRouting() *mux.Router { router := mux.NewRouter() @@ -32,75 +56,90 @@ func (qmc *QuesmaManagementConsole) createRouting() *mux.Router { qmc.initPprof(router) - router.HandleFunc("/", func(writer http.ResponseWriter, req *http.Request) { + // just for oauth compliance + router.HandleFunc("/auth/{provider}", gothic.BeginAuthHandler) + router.HandleFunc("/auth/{provider}/callback", authCallbackHandler) + + // our logic for login + router.HandleFunc("/login-with-elasticsearch", qmc.HandleElasticsearchLogin) + + authenticatedRoutes := router.PathPrefix("/").Subrouter() + if qmc.cfg.Elasticsearch.User == "" && qmc.cfg.Elasticsearch.Password == "" { + logger.Warn().Msg("admin console authentication is disabled") + } else { + authenticatedRoutes.Use(authMiddleware) + } + + authenticatedRoutes.HandleFunc("/", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateDashboard() _, _ = writer.Write(buf) }) // /dashboard is referenced in docs and should redirect to / - router.HandleFunc("/dashboard", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/dashboard", func(writer http.ResponseWriter, req *http.Request) { http.Redirect(writer, req, "/", http.StatusSeeOther) }) - router.HandleFunc("/live", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/live", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateLiveTail() _, _ = writer.Write(buf) }) - router.HandleFunc("/table_resolver", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/table_resolver", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateTableResolver() _, _ = writer.Write(buf) }) - router.HandleFunc("/table_resolver/ask", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/table_resolver/ask", func(writer http.ResponseWriter, req *http.Request) { prompt := req.PostFormValue("prompt") buf := qmc.generateTableResolverAnswer(prompt) _, _ = writer.Write(buf) }) - router.HandleFunc("/tables/reload", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/tables/reload", func(writer http.ResponseWriter, req *http.Request) { + qmc.logManager.ReloadTables() buf := qmc.generateTables() _, _ = writer.Write(buf) }).Methods("POST") - router.HandleFunc("/tables", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/tables", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateTables() _, _ = writer.Write(buf) }) - router.HandleFunc("/tables/common_table_stats", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/tables/common_table_stats", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateQuesmaAllLogs() _, _ = writer.Write(buf) }) - router.HandleFunc("/schemas", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/schemas", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateSchemas() _, _ = writer.Write(buf) }) - router.HandleFunc("/telemetry", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/telemetry", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateTelemetry() _, _ = writer.Write(buf) }) - router.HandleFunc("/data-sources", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/data-sources", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateDatasourcesPage() _, _ = writer.Write(buf) }) - router.HandleFunc("/routing-statistics", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/routing-statistics", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateRouterStatisticsLiveTail() _, _ = writer.Write(buf) }) - router.HandleFunc("/ingest-statistics", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/ingest-statistics", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateIngestStatistics() _, _ = writer.Write(buf) }) - router.HandleFunc("/statistics-json", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/statistics-json", func(writer http.ResponseWriter, req *http.Request) { jsonBody, err := json.Marshal(stats.GlobalStatistics) if err != nil { logger.Error().Msgf("Error marshalling statistics: %v", err) @@ -111,78 +150,92 @@ func (qmc *QuesmaManagementConsole) createRouting() *mux.Router { writer.WriteHeader(200) }) - router.HandleFunc("/panel/routing-statistics", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/panel/routing-statistics", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateRouterStatistics() _, _ = writer.Write(buf) }) - router.HandleFunc("/panel/statistics", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/panel/statistics", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateStatistics() _, _ = writer.Write(buf) }) - router.HandleFunc("/panel/queries", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/panel/queries", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateQueries() _, _ = writer.Write(buf) }) - router.HandleFunc("/panel/dashboard", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/panel/dashboard", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateDashboardPanel() buf = append(buf, qmc.generateDashboardTrafficPanel()...) _, _ = writer.Write(buf) }) - router.HandleFunc("/panel/data-sources", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/panel/data-sources", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateDatasources() _, _ = writer.Write(buf) }) - router.PathPrefix("/request-id/{requestId}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/request-id/{requestId}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) buf := qmc.generateReportForRequestId(vars["requestId"]) _, _ = writer.Write(buf) }) - router.PathPrefix("/error/{reason}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/error/{reason}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) buf := qmc.generateErrorForReason(vars["reason"]) _, _ = writer.Write(buf) }) - router.Path("/unsupported-requests").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.Path("/unsupported-requests").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { buf := qmc.generateReportForUnsupportedRequests() _, _ = writer.Write(buf) }) - router.PathPrefix("/unsupported-requests/{reason}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/unsupported-requests/{reason}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) buf := qmc.generateReportForUnsupportedType(vars["reason"]) _, _ = writer.Write(buf) }) - router.PathPrefix("/requests-by-str/{queryString}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/requests-by-str/{queryString}").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) buf := qmc.generateReportForRequestsWithStr(vars["queryString"]) _, _ = writer.Write(buf) }) - router.PathPrefix("/requests-with-error/").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/requests-with-error/").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { buf := qmc.generateReportForRequestsWithError() _, _ = writer.Write(buf) }) - router.PathPrefix("/requests-with-warning/").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/requests-with-warning/").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { buf := qmc.generateReportForRequestsWithWarning() _, _ = writer.Write(buf) }) - router.PathPrefix("/request-id").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/request-id").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { // redirect to / http.Redirect(writer, r, "/", http.StatusSeeOther) }) - router.PathPrefix("/requests-by-str").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + authenticatedRoutes.PathPrefix("/requests-by-str").HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { // redirect to / http.Redirect(writer, r, "/", http.StatusSeeOther) }) - router.HandleFunc("/queries", func(writer http.ResponseWriter, req *http.Request) { + authenticatedRoutes.HandleFunc("/queries", func(writer http.ResponseWriter, req *http.Request) { buf := qmc.generateQueries() _, _ = writer.Write(buf) }) + authenticatedRoutes.HandleFunc("/logout", func(writer http.ResponseWriter, req *http.Request) { + session, err := store.Get(req, quesmaSessionName) + if err != nil { + http.Redirect(writer, req, "/login", http.StatusTemporaryRedirect) + return + } + session.Options.MaxAge = -1 + session.Values = make(map[interface{}]interface{}) + err = session.Save(req, writer) + if err != nil { + logger.Error().Msgf("Could not delete user session: %v", err) + } + http.Redirect(writer, req, "/dashboard", http.StatusTemporaryRedirect) + }) - router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(uiFs)))) + authenticatedRoutes.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(uiFs)))) return router } @@ -194,7 +247,29 @@ func (qmc *QuesmaManagementConsole) initPprof(router *mux.Router) { router.HandleFunc("/debug/pprof/trace", pprof.Trace) } +var store = sessions.NewCookieStore([]byte("test")) + +func authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !isAlreadyAuthenticated(r) { + logger.Warn().Msgf("User not authenticated, redirecting to login page") + http.Redirect(w, r, "/auth/elasticsearch", http.StatusTemporaryRedirect) + return + } + next.ServeHTTP(w, r) + }) +} + +func isAlreadyAuthenticated(r *http.Request) bool { + session, err := store.Get(r, quesmaSessionName) + userID, ok := session.Values["userID"].(string) + return ok && userID != "" && err == nil +} + func (qmc *QuesmaManagementConsole) newHTTPServer() *http.Server { + goth.UseProviders( + NewElasticsearchAuthProvider(), + ) return &http.Server{ Addr: ":" + uiTcpPort, Handler: qmc.createRouting(), diff --git a/quesma/quesma/ui/es_auth_provider.go b/quesma/quesma/ui/es_auth_provider.go new file mode 100644 index 000000000..966f6375d --- /dev/null +++ b/quesma/quesma/ui/es_auth_provider.go @@ -0,0 +1,62 @@ +// Copyright Quesma, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +package ui + +import ( + "encoding/json" + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +// ElasticsearchAuthProvider implements the `goth.Provider` for accessing Elasticsearch. +// It is not a real oAuth provider, because essentially it just redirects to Quesma's login page which handles auth via Elasticsearch, +// but in the future this would allow us adding any auth providers we want in a very easy way. +type ElasticsearchAuthProvider struct { + providerName string +} + +func NewElasticsearchAuthProvider() *ElasticsearchAuthProvider { + return &ElasticsearchAuthProvider{ + providerName: "elasticsearch", + } +} + +type ElasticsearchSession struct{} + +func (e ElasticsearchSession) GetAuthURL() (string, error) { + return "http://localhost:9999/login-with-elasticsearch", nil +} + +func (e ElasticsearchSession) Marshal() string { + b, _ := json.Marshal(e) + return string(b) +} + +func (e ElasticsearchSession) Authorize(provider goth.Provider, params goth.Params) (string, error) { + return "", nil +} + +func (e ElasticsearchAuthProvider) Name() string { return e.providerName } + +func (e *ElasticsearchAuthProvider) SetName(name string) { e.providerName = name } + +func (e ElasticsearchAuthProvider) BeginAuth(state string) (goth.Session, error) { + return &ElasticsearchSession{}, nil +} + +func (e ElasticsearchAuthProvider) UnmarshalSession(s string) (goth.Session, error) { + return nil, nil +} + +func (e ElasticsearchAuthProvider) FetchUser(session goth.Session) (goth.User, error) { + return goth.User{}, nil +} + +func (e ElasticsearchAuthProvider) Debug(b bool) {} + +func (e ElasticsearchAuthProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, nil +} + +func (e ElasticsearchAuthProvider) RefreshTokenAvailable() bool { return false } diff --git a/quesma/quesma/ui/html_utils.go b/quesma/quesma/ui/html_utils.go index a1d2d96ba..6fa4b5c5d 100644 --- a/quesma/quesma/ui/html_utils.go +++ b/quesma/quesma/ui/html_utils.go @@ -76,7 +76,7 @@ func generateTopNavigation(target string) []byte { buffer.Html(` class="active"`) } buffer.Html(`>Data sources`) - + buffer.Html(`
  • Logout
  • `) buffer.Html("\n\n") buffer.Html("\n\n") diff --git a/quesma/quesma/ui/login.go b/quesma/quesma/ui/login.go new file mode 100644 index 000000000..7e5096a2d --- /dev/null +++ b/quesma/quesma/ui/login.go @@ -0,0 +1,66 @@ +// Copyright Quesma, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +package ui + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "quesma/elasticsearch" + "quesma/logger" +) + +func (qmc *QuesmaManagementConsole) generateLoginForm() []byte { + buffer := newBufferWithHead() + buffer.Html(``) + buffer.Html(``) + buffer.Html(``) + buffer.Html(``) + buffer.Html(``) + return buffer.Bytes() +} + +func (qmc *QuesmaManagementConsole) HandleElasticsearchLogin(writer http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet { + if isAlreadyAuthenticated(req) { + http.Redirect(writer, req, "/dashboard", http.StatusSeeOther) + return + } + writer.Header().Set("Content-Type", "text/html") + writer.Write(qmc.generateLoginForm()) + } else if req.Method == http.MethodPost { + username := req.FormValue("username") + password := req.FormValue("password") + if qmc.isValidElasticsearchUser(username, password) { + session, _ := store.Get(req, quesmaSessionName) + session.Values["userID"] = username + session.Save(req, writer) + http.Redirect(writer, req, "/dashboard", http.StatusSeeOther) + } else { + logger.Warn().Msgf("Invalid credentials for user [%s], could not login with Elasticsearch", username) + http.Error(writer, "Invalid credentials", http.StatusUnauthorized) + } + } else { + http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func (qmc *QuesmaManagementConsole) isValidElasticsearchUser(username, password string) bool { + ctx := context.Background() + authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) + return elasticsearch.NewSimpleClient(&qmc.cfg.Elasticsearch).Authenticate(ctx, authHeader) +}