diff --git a/README.md b/README.md index 7d7fd39..2bb19eb 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,99 @@ curl -X POST -H "content-type:application/json" -d '{"query":"query FindEventRec curl -X POST -H "content-type:application/json" -d '{"query":"query FindEventReceiverGroups($id: ID!){event_receiver_groups(id: $id) {id,name,type,version,description,enabled,event_receiver_ids,created_at,updated_at}}","variables":{"id":"01HKX90FKWQZ49F6H5V5NQT95Z"}}' http://localhost:8042/api/v1/graphql/query ``` +## Keycloak + +Follow this guide to get started https://www.keycloak.org/getting-started/getting-started-docker. + +That'll get you a new user and client. Unlike the example, set `client authentication` to `on`, otherwise, you can't +access the client credentials tab in the admin console http://localhost:8083/admin/master/console/#/ +/clients//credentials. + +To start up Keycloak. I changed the port number because the default 8080 is in use by EPR itself. + +```bash +docker run -p 8083:8083 -e KC_HTTP_PORT=8083 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ + quay.io/keycloak/keycloak:24.0.1 start-dev +``` + +http://localhost:8083/admin/master/console/#/test/clients +https://github.com/coreos/go-oidc +https://github.com/coreos/go-oidc/blob/v3/example/userinfo/app.go + +Need to grab a token from Keycloak first. You can get oidc config info from +here http://localhost:8083/realms/test/.well-known/openid-configuration +Log into your new realm with your account from here: http://localhost:8083/realms//account. + +Once you've got that, + +```bash +export access_token=$(\ + curl -X POST http://localhost:8083/realms/test/protocol/openid-connect/token \ + --user epr-client-id:rGMO0kRpvUj3XD9It678AoTlgMtGxItJ \ + -d 'username=testbob&password=abc123&grant_type=password' | jq --raw-output '.access_token'\ +) +``` + +The `--user` flag is the credentials for the client connecting to Keycloak. In our case, that'll be EPR. The payload +content depends on the grant type. For our purposes, password is easiest. The username and password will be those of +the user you created in the tutorial. + +I think the flow is roughly: + +1. User hits a logon endpoint and gets redirected to Keycloak. +2. EPR passes up client credentials plus user info. +3. Keycloak returns a token +4. EPR passes token to user. + +Although, I think we could shorten it to just get a JWT straight from Keycloak without passing it back through EPR... +I'll have to play with it. + +Once you have the access token, use it to make requests to EPR. + +```bash +curl -X GET -H "Authorization: Bearer $access_token" -w "%{http_code}\n" http://localhost:8042/api/oidctest +``` + +I ran into some problems with the audience not being set on the +JWT. [See this post]( https://stackoverflow.com/questions/53550321/keycloak-gatekeeper-aud-claim-and-client-id-do-not-match) +for more info. Options are slightly different than what is +described. [This video](https://www.youtube.com/watch?v=G2QVhUAEylc) was more helpful. + +To create a receiver with an auth token. + +```bash +curl --location --request POST 'http://localhost:8042/api/v1/receivers' \ +--header 'Content-Type: application/json' \ +--header "authorization: Bearer $access_token" \ +--data-raw '{ + "name": "foobar", + "type": "whatever", + "version": "1.1.2", + "description": "it does stuff", + "enabled": true, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } +} +}' +``` + +https://documenter.getpostman.com/view/7294517/SzmfZHnd#intro +https://suedbroecker.net/2020/08/04/how-to-create-a-new-realm-with-the-keycloak-rest-api/ +https://documenter.getpostman.com/view/7294517/SzmfZHnd#cf71cd19-6910-467f-b04e-3f3bf5539d81 +https://github.com/thomassuedbroecker/keycloak-create-realm-bash < this one is good. + +Make sure you get the right version of [the docs](https://www.keycloak.org/docs-api/latest/rest-api/index.html). Google will sometimes give you older versions that you may not want. + +Admin CLI get's packaged in with the container. +https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/admin-cli.html + +It's actually a jar that gets invoked by the `kcadm.sh` script. + ## Contributing We welcome your contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/cli/go.mod b/cli/go.mod index dd93e80..5ca78cd 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -44,9 +44,9 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gorm.io/datatypes v1.2.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 076d849..9c27dc4 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -186,6 +186,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -214,6 +215,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= diff --git a/docker-compose.services.yaml b/docker-compose.services.yaml index ed49f3e..78fce25 100644 --- a/docker-compose.services.yaml +++ b/docker-compose.services.yaml @@ -86,3 +86,20 @@ services: - "8080:8080" depends_on: - redpanda + keycloak: + image: "quay.io/keycloak/keycloak:24.0.1" + restart: unless-stopped + command: + - start-dev + environment: + KC_HTTP_PORT: 8083 + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_LOG_LEVEL: debug + # add the admin CLI to the path. + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/keycloak/bin + ports: + - "8083:8083" + # TODO: rip this out. For debugging only. + volumes: + - ./:/work diff --git a/docs/how-to/watcher/go.mod b/docs/how-to/watcher/go.mod index 5f99b2f..91705a5 100644 --- a/docs/how-to/watcher/go.mod +++ b/docs/how-to/watcher/go.mod @@ -50,9 +50,9 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/datatypes v1.2.0 // indirect diff --git a/docs/how-to/watcher/go.sum b/docs/how-to/watcher/go.sum index 11abbb9..348a079 100644 --- a/docs/how-to/watcher/go.sum +++ b/docs/how-to/watcher/go.sum @@ -212,6 +212,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -225,6 +226,7 @@ golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -249,6 +251,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= diff --git a/go.mod b/go.mod index 6d6155b..ba4c0f4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/Shopify/sarama v1.38.1 github.com/adrg/xdg v0.4.0 + github.com/coreos/go-oidc/v3 v3.10.0 github.com/go-chi/chi/v5 v5.0.11 github.com/go-chi/cors v1.2.1 github.com/go-chi/httplog v0.3.0 @@ -20,7 +21,7 @@ require ( github.com/twmb/franz-go v1.14.4 github.com/xdg/scram v1.0.5 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.22.0 golang.org/x/sync v0.5.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.2.0 @@ -30,11 +31,14 @@ require ( ) require ( + github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/zerolog v1.29.0 // indirect github.com/twmb/franz-go/pkg/kmsg v1.6.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + google.golang.org/appengine v1.6.8 // indirect ) require ( @@ -87,10 +91,10 @@ require ( github.com/stretchr/testify v1.8.2 github.com/subosito/gotenv v1.4.2 // indirect github.com/xdg/stringprep v1.0.3 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gorm.io/driver/mysql v1.4.7 // indirect ) diff --git a/go.sum b/go.sum index 8717bec..7adedd4 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= +github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -97,6 +99,8 @@ github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIo github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= @@ -135,6 +139,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD 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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -410,8 +415,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -482,8 +487,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -493,6 +498,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -556,8 +563,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= @@ -570,6 +577,7 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -658,6 +666,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -722,8 +732,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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/keycloak-setup.sh b/keycloak-setup.sh new file mode 100644 index 0000000..bff7b69 --- /dev/null +++ b/keycloak-setup.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# This script must be run somewhere with access to the kcadm.sh script that invokes the Keycloak Admin CLI. +REALM=junk + +kcadm.sh config credentials --server http://localhost:8083 --realm master --user admin --password admin +kcadm.sh create realms -s realm=$REALM -s enabled=true -o +kcadm.sh create users -r $REALM -s username=bob -s email=bob@example.com -s enabled=true -o --fields id,username +kcadm.sh create clients -r $REALM -s clientId=epr-client -o +kcadm.sh create client-scopes -r $REALM -s name=epr-client-scope #c7683fc0-8e21-4c22-b1ee-9ac27f3ac135 TODO: need more than this, otherwise we hit an error trying to view the scope in the UI. +kcadm.sh create client-scopes/c7683fc0-8e21-4c22-b1ee-9ac27f3ac135/mappers -r $REALM -o +# TODO: we can read from json files with the CLI which may make more sense long term. + +access_token=$( curl -d "client_id=admin-cli" -d "username=admin" -d "password=admin" -d "grant_type=password" "http://localhost:8083/realms/master/protocol/openid-connect/token" | sed -n 's|.*"access_token":"\([^"]*\)".*|\1|p') + +# Post a new realm. The docs are broke, so I had to wing it by posting everything. + +curl -H "Authorization: bearer $access_token" -H "content-type: application/json" -X POST \ + -d '@realm.json' \ + http://localhost:8083/admin/realms + +# need a user + +curl -H "Authorization: bearer $access_token" -H "content-type: application/json" -X POST \ + -d '{ + "username": "bob", + "email": "bob@example.com", + "emailVerified": true, + "enabled": true, + "firstName": "Bob", + "lastName": "Testy", + "credentials": [ + { + "temporary": false, + "type": "password", + "value": "abc123" + } + ] +}' http://localhost:8083/admin/realms/junk/users + +# need a client + +curl -H "Authorization: bearer $access_token" -H "content-type: application/json" \ + http://localhost:8083/admin/realms/junk/clients + +curl -H "Authorization: bearer $access_token" -H "content-type: application/json" -X POST \ + -d '{ + "clientId": "dummy-client", + "name": "my-dummy-client", + "directAccessGrantsEnabled": true +}' http://localhost:8083/admin/realms/junk/clients + +# TODO: need the audience mapper and audience resolve + +curl -H "Authorization: bearer $access_token" -H "content-type: application/json" -X POST \ + -d '{ + "name": "epr-client-id-dedicated" +}' http://localhost:8083/admin/realms/junk/client-scopes + +# need a client secret +curl -H "Authorization: bearer $access_token" -H "content-type: application/json" -X POST \ + -d '{ + "name": "epr-client-id-dedicated" +}' http://localhost:8083/admin/realms/junk/client-scopes/9940fe9c-005b-4d52-b25f-6865bbd9d60e/scope-mappings/clients/{asdf} + + +#curl -H "Authorization: bearer $access_token" -H "content-type: application/json" http://localhost:8083/admin/realms/junk/users + +#docker run --user=1000 --env=KC_HTTP_PORT=8083 --env=KEYCLOAK_ADMIN=admin --env=KEYCLOAK_ADMIN_PASSWORD=admin --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --env=LANG=en_US.UTF-8 --env=KC_RUN_IN_CONTAINER=true --env=KC_LOG_LEVEL=DEBUG -p 8083:8083 --restart=no --runtime=runc quay.io/keycloak/keycloak:24.0.1 start-dev \ No newline at end of file diff --git a/pkg/api/api.go b/pkg/api/api.go index db30d3f..21edc91 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -4,21 +4,22 @@ package api import ( + "context" "fmt" - "log" - "log/slog" - "net/http" - "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httplog" "github.com/go-chi/render" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/sassoftware/event-provenance-registry/pkg/auth" "github.com/sassoftware/event-provenance-registry/pkg/config" "github.com/sassoftware/event-provenance-registry/pkg/message" "github.com/sassoftware/event-provenance-registry/pkg/status" "github.com/sassoftware/event-provenance-registry/pkg/storage" + "log" + "log/slog" + "net/http" ) // Initialize starts the database, kafka message producer, middleware, and endpoints @@ -32,6 +33,10 @@ func Initialize(db *storage.Database, msgProducer message.TopicProducer, cfg *co log.Fatal(err) } + verification, err := auth.NewMiddleware(context.TODO()) + if err != nil { + return nil, err + } // Create a new router router := chi.NewRouter() // Add some middleware to our router @@ -59,6 +64,7 @@ func Initialize(db *storage.Database, msgProducer message.TopicProducer, cfg *co }) router.Route("/api", func(r chi.Router) { + r.Use(verification.Handle()) r.Get("/", s.Rest.ServeOpenAPIDoc(cfg.ResourceDir)) r.Route("/v1", func(r chi.Router) { r.Use(crs.Handler) @@ -99,7 +105,7 @@ func Initialize(db *storage.Database, msgProducer message.TopicProducer, cfg *co router.Route("/healthz", func(r chi.Router) { r.Get("/liveness", s.CheckLiveness()) r.Get("/readiness", s.CheckReadiness()) - r.Get("/status", s.CheckStatus(cfg)) + r.With(verification.Handle()).Get("/status", s.CheckStatus(cfg)) }) // turn on the profiler in debug mode @@ -113,12 +119,13 @@ func Initialize(db *storage.Database, msgProducer message.TopicProducer, cfg *co // endpoint for serving Prometheus metrics router.Route("/metrics", func(r chi.Router) { + r.Use(verification.Handle()) r.Get("/", promhttp.Handler().(http.HandlerFunc)) }) // Separate, to ensure no authentication required. router.Route("/api/v1/graphql", func(r chi.Router) { - r.Use(crs.Handler) + r.Use(crs.Handler, verification.Handle()) r.Get("/", s.GraphQL.ServerGraphQLDoc()) r.Post("/query", s.GraphQL.GraphQLHandler()) }) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 04cc6e3..75a50d2 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -3,4 +3,88 @@ package auth -type NewAuthorizer interface{} +import ( + "context" + "github.com/coreos/go-oidc/v3/oidc" + "log/slog" + "net/http" + "strings" +) + +type Middleware struct { + verifier *oidc.IDTokenVerifier +} + +func NewMiddleware(ctx context.Context) (*Middleware, error) { + provider, err := oidc.NewProvider(ctx, "http://localhost:8083/realms/test") + if err != nil { + return nil, err + } + + clientID := "epr-client-id" + oidcConfig := &oidc.Config{ + ClientID: clientID, + } + + verifier := provider.Verifier(oidcConfig) + + return &Middleware{ + verifier: verifier, + }, nil +} + +func (m *Middleware) Handle() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + bearer := BearerToken(r) + + if bearer == "" { + slog.Error("bearer token missing") + w.WriteHeader(http.StatusUnauthorized) + return + } + + // authHeader := r.Header.Get("authorization") + // split := strings.Split(authHeader, " ") + // if len(split) != 2 { + // slog.Error("more authorization header pieces than expected") + // w.WriteHeader(http.StatusBadRequest) + // return + // } + // if !strings.EqualFold(split[0], "bearer") { + // slog.Error("unexpected authorization header type", "type", split[0]) + // } + // + // idToken, err := verifier.Verify(r.Context(), split[1]) + // if err != nil { + // slog.Error("authorization failed", "error", err) + // w.WriteHeader(http.StatusUnauthorized) + // return + // } + + _, err := m.verifier.Verify(r.Context(), bearer) + if err != nil { + slog.Error(err.Error()) + w.WriteHeader(http.StatusUnauthorized) + return + } + + // TODO: validate any roles and stuff. + + // TODO: do we need this context key? + //ctx := context.WithValue(r.Context(), "", token) + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +func BearerToken(req *http.Request) string { + auth := req.Header.Get("Authorization") + const prefix = "Bearer " + if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { + return "" + } + return auth[len(prefix):] +} diff --git a/realm.json b/realm.json new file mode 100644 index 0000000..95791c5 --- /dev/null +++ b/realm.json @@ -0,0 +1,134 @@ +{ + "realm": "junk", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": {}, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "internationalizationEnabled": false, + "supportedLocales": [], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/tests/go.mod b/tests/go.mod index 39ccd22..893d88c 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -28,7 +28,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/datatypes v1.2.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index e7a15e3..a85da55 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -143,6 +143,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=