diff --git a/go.mod b/go.mod index 8e531c0..b32ba98 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,13 @@ module github.com/postfinance/discovery -go 1.22 +go 1.22.4 require ( connectrpc.com/connect v1.16.2 github.com/alecthomas/kong v0.7.1 - github.com/coreos/go-oidc/v3 v3.5.0 github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/golang/protobuf v1.5.2 + github.com/golang/protobuf v1.5.3 github.com/google/renameio v1.0.1 - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/pkg/errors v0.9.1 github.com/postfinance/flash v0.5.0 @@ -19,12 +17,11 @@ require ( github.com/prometheus/client_golang v1.14.0 github.com/satori/go.uuid v1.2.0 github.com/sethvargo/go-retry v0.2.4 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.9.0 github.com/zbindenren/king v0.3.1 github.com/zbindenren/sfmt v0.1.0 go.uber.org/zap v1.24.0 - golang.org/x/oauth2 v0.5.0 - golang.org/x/term v0.18.0 + golang.org/x/term v0.21.0 google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2 google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.33.0 @@ -36,10 +33,12 @@ require ( github.com/BurntSushi/toml v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coreos/go-oidc/v3 v3.10.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect + github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -49,16 +48,19 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/sykesm/zap-logfmt v0.0.4 // indirect + gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240618053820-18382f9e0efb // indirect + gitlab.pnet.ch/linux/go/crpcauth v0.1.1-0.20240617142938-6f35847f6293 // indirect go.etcd.io/etcd/api/v3 v3.5.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect go.etcd.io/etcd/client/v3 v3.5.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/appengine v1.6.8 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.80.1 // indirect diff --git a/go.sum b/go.sum index 60fca0c..bfdf8b5 100644 --- a/go.sum +++ b/go.sum @@ -13,18 +13,12 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 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 v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -89,8 +83,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= -github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= +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-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -106,7 +100,10 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -126,8 +123,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME 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/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -178,8 +175,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw 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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -397,8 +395,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/sykesm/zap-logfmt v0.0.4 h1:U2WzRvmIWG1wDLCFY3sz8UeEmsdHQjHFNlIdmroVFaI= github.com/sykesm/zap-logfmt v0.0.4/go.mod h1:AuBd9xQjAe3URrWT1BBDk2v2onAZHkZkWRMiYZXiZWA= @@ -419,6 +418,24 @@ github.com/zbindenren/king v0.3.1 h1:Q2eRjcA85DL9zrMPv3DK95ve+ALF/5+vubQiX1++HsE github.com/zbindenren/king v0.3.1/go.mod h1:Ba2jL6BDjRnn6rES8HCZjh3zRcaywctK8dN1jGPyP5w= github.com/zbindenren/sfmt v0.1.0 h1:3jsWp9A85MsYpjMr7WWW5K5omHvdRmMw4EShx6+SFZI= github.com/zbindenren/sfmt v0.1.0/go.mod h1:VwVBAQUbqRU7wiHoyO9xB19xemK6RLz2rtrM21pbAWI= +gitlab.pnet.ch/linux/go/auth v0.1.0 h1:DBfgJODkKQErdfH619o/qrsE8V2c3LdFnblOwkqqgPs= +gitlab.pnet.ch/linux/go/auth v0.1.0/go.mod h1:a3K4SvkWWEiyyT9lw2F4+dQyZsTpv/d+y3JISFE1VcQ= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240617110906-9ea041c5e1f4 h1:an73hzmotTtGu0mwohi1BOHiuoEQnBFA74m6yt5mPCo= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240617110906-9ea041c5e1f4/go.mod h1:o4MYheyU1KlIfdAPOa0EU082TJz8mKauMDxGI872ImE= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240617142830-3de02991df16 h1:RSd876Ermm6Ca0BuyvkLgsfekrhqbttRlhHCtdX1BG4= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240617142830-3de02991df16/go.mod h1:o4MYheyU1KlIfdAPOa0EU082TJz8mKauMDxGI872ImE= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240618041542-804208a58c47 h1:0NQCChGvMyXHJf7uup5wWYJ4fb2vVxI0h+uGcqQr6Jo= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240618041542-804208a58c47/go.mod h1:o4MYheyU1KlIfdAPOa0EU082TJz8mKauMDxGI872ImE= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240618053820-18382f9e0efb h1:JEhGejqQEpw/OXkbp7A1OHnwUFb5HsCJQnCGhKIIgJo= +gitlab.pnet.ch/linux/go/auth v0.1.1-0.20240618053820-18382f9e0efb/go.mod h1:o4MYheyU1KlIfdAPOa0EU082TJz8mKauMDxGI872ImE= +gitlab.pnet.ch/linux/go/crpcauth v0.1.0 h1:V1YNvo7IkvS02PokVGH/CXbNj+MAF7pq6q7I8QTD4ss= +gitlab.pnet.ch/linux/go/crpcauth v0.1.0/go.mod h1:H40U9ZsPOokz437w63wqAckHJ8DxM8REaVHK5keRUZY= +gitlab.pnet.ch/linux/go/crpcauth v0.1.1-0.20240617122209-9cdc72191545 h1:fGWJJiV9sO6z6/arNgJ7mb7GhAgnA45zFKLW9OwBiPc= +gitlab.pnet.ch/linux/go/crpcauth v0.1.1-0.20240617122209-9cdc72191545/go.mod h1:09ZqowGupg6Ad4l200Iem45deH4pmjVXvSe2tvzhItY= +gitlab.pnet.ch/linux/go/crpcauth v0.1.1-0.20240617142938-6f35847f6293 h1:Vp7iBNlJTRSlFRzH3nXeoGBtHVAhFixj50kDSM8V4s0= +gitlab.pnet.ch/linux/go/crpcauth v0.1.1-0.20240617142938-6f35847f6293/go.mod h1:Nd3CdQEbo34SzonzWTIeK0BBAEdpuZa3MN43694BqbY= +gitlab.pnet.ch/linux/grpc/connect v0.39.9 h1:kK5hmjTNNaEl10y9mlpDLBzKTrv3nFHWRPo3j8LjPaY= +gitlab.pnet.ch/linux/grpc/connect v0.39.9/go.mod h1:vNTQYtU3hlU00rCUFRpm7IJWjFWKz643Py76tYVpUhc= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= @@ -488,13 +505,12 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf 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-20190911031432-227b76d455e7/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-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 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= @@ -567,10 +583,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 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= @@ -578,9 +592,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +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= @@ -644,14 +657,12 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc 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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 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= @@ -660,9 +671,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/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.5.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= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -746,8 +757,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -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= @@ -812,7 +823,6 @@ 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.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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/internal/auth/ask.go b/internal/auth/ask.go deleted file mode 100644 index 38adf0d..0000000 --- a/internal/auth/ask.go +++ /dev/null @@ -1,111 +0,0 @@ -package auth - -import ( - "bufio" - "fmt" - "io" - "strings" - "syscall" - - "golang.org/x/term" -) - -// Credentials contains username and password. -type Credentials struct { - Username string - Password string -} - -// Option is a functional option to configure the Asker. -type Option func(a *Asker) - -// NewAsker creates a new Asker. -func NewAsker(opts ...Option) *Asker { - a := Asker{ - prompt: "Enter Username", - } - - for _, opt := range opts { - opt(&a) - } - - return &a -} - -// WithDfltUsername offers a dflt username in prompt. -func WithDfltUsername(username string) Option { - return func(a *Asker) { - a.username = username - } -} - -// WithPrompt overrides default `Enter Username` prompt. -func WithPrompt(prompt string) Option { - return func(a *Asker) { - stripped := strings.TrimRight(prompt, ":") - a.prompt = stripped - } -} - -// Asker asks for username and password. -type Asker struct { - prompt string - username string -} - -// Ask aks the username and password. -func (a *Asker) Ask(in io.Reader, out io.Writer) (*Credentials, error) { - var ( - c Credentials - prompt string - ) - - prompt = a.prompt - - if a.username != "" { - prompt = fmt.Sprintf("%s (%s)", prompt, a.username) - } - - prompt += ": " - - // username prompt, with offered username - fmt.Fprint(out, prompt) - - reader := bufio.NewReader(in) - - userInput, err := reader.ReadString('\n') - if err != nil { - return nil, fmt.Errorf("reading from stdin: %w", err) - } - - c.Username = strings.TrimSpace(userInput) - - if c.Username == "" { - // set the default username - c.Username = a.username - } - - p, err := readPassword("Enter SecurID Password", out) - if err != nil { - return nil, err - } - - c.Password = strings.TrimSpace(p) - - return &c, nil -} - -func readPassword(prompt string, out io.Writer) (string, error) { - fmt.Fprintf(out, "%s: ", prompt) - - //nolint:unconvert // needs to be done for windows - // conversion for GOOS=windows - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - return "", fmt.Errorf("reading password from stdin: %w", err) - } - - fmt.Println("") - - return strings.TrimSpace(string(bytePassword)), nil -} diff --git a/internal/auth/ask_test.go b/internal/auth/ask_test.go deleted file mode 100644 index 7496337..0000000 --- a/internal/auth/ask_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package auth - -import ( - "bytes" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestAsk(t *testing.T) { - r := require.New(t) - - asker := NewAsker( - WithDfltUsername("test"), - WithPrompt("Enter"), - ) - - r.NotNil(asker) - - var b bytes.Buffer - creds, err := asker.Ask(strings.NewReader("Test\n"), &b) - - r.Contains(err.Error(), "reading password from stdin") - r.Nil(creds) - r.Equal("Enter (test): Enter SecurID Password: ", b.String()) -} diff --git a/internal/auth/auth.go b/internal/auth/auth.go deleted file mode 100644 index edf7e0d..0000000 --- a/internal/auth/auth.go +++ /dev/null @@ -1,246 +0,0 @@ -// Package auth handles openid connect and jwt (for access tokens) authentication and -// authorization. -package auth - -import ( - "context" - "strings" - - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" - "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// Func creates a authentication function that can be used in combination with grpc middleware. -// -// The function verifies two kinds of tokens: -// First, it verfifies with the given key, if the token is a valid jwt token. If the token is valid access is granted -// for machines. -// -// If the above fails it checks if the token is a valid oidc token. If successful access is granted to a user. -// -// In both (successful) cases it extracts the user and adds it in the current context. -// -// Reflection and list requests are not authorized. -func Func(verifier Verifier, th *TokenHandler, l *zap.SugaredLogger, claimConfig ClaimConfig) func(ctx context.Context) (context.Context, error) { - return func(ctx context.Context) (context.Context, error) { - methodName := methodNameFromContext(ctx) - if strings.HasPrefix(methodName, "/grpc.reflection") { - return ctx, nil - } - - token, err := grpc_auth.AuthFromMD(ctx, "bearer") - if err != nil { - return nil, status.Errorf(codes.Unauthenticated, "failed to get authentication token: %s", err) - } - - // machine tokens - ok, err := th.IsMachine(token) - if err != nil { - return nil, status.Errorf(codes.Unauthenticated, "failed to parse machine token: %s", err) - } - - if ok { - u, err := th.Validate(token) - if err != nil { - return nil, status.Errorf(codes.Unauthenticated, "machine token is invalid: %s", err) - } - - l.Debugw("grpc authentication", - "methodName", methodName, - "isUserToken", u.IsUser(), - "name", u.Username, - ) - - return context.WithValue(ctx, userKey, u), nil - } - - // personal personal - idToken, err := verifier.Verify(ctx, token) - if err != nil { - return nil, status.Errorf(codes.PermissionDenied, "personal token is not valid: %s", err) - } - - c := claims{} - - err = idToken.Claims(&c) - if err != nil { - return nil, status.Errorf(codes.Internal, "could not get claims: %s", err) - } - - u := User{ - Username: claimConfig.Username(c), - Roles: claimConfig.Roles(c), - Kind: UserToken, - } - - l.Infow("grpc authentication", - "methodName", methodName, - "isUserToken", u.IsUser(), - "name", u.Username, - "roles", strings.Join(u.Roles, ","), - ) - - return context.WithValue(ctx, userKey, u), nil - } -} - -// UnaryAuthorizeInterceptor authorizes GRPC requests. -func UnaryAuthorizeInterceptor(rwRoles ...string) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - if err := authorizeUser(ctx, rwRoles...); err != nil { - return nil, err - } - - return handler(ctx, req) - } -} - -// UnaryMethodNameInterceptor adds GRPC method name to context. -func UnaryMethodNameInterceptor() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - wrappedCtx := context.WithValue(ctx, methodNameKey, info.FullMethod) - - return handler(wrappedCtx, req) - } -} - -// StreamMethodNameInterceptor adds GRPC method name to context. -func StreamMethodNameInterceptor() grpc.StreamServerInterceptor { - return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - wrapped := grpc_middleware.WrapServerStream(stream) - wrapped.WrappedContext = context.WithValue(stream.Context(), methodNameKey, info.FullMethod) - - return handler(srv, wrapped) - } -} - -// StreamAuthorizeInterceptor authorizes GRPC streams. -func StreamAuthorizeInterceptor(rwRoles ...string) grpc.StreamServerInterceptor { - return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if err := authorizeUser(stream.Context(), rwRoles...); err != nil { - return err - } - - return handler(srv, stream) - } -} - -// ClaimConfig configures how to get username and roles from claims. -type ClaimConfig struct { - username string - roles string -} - -// NewClaimConfig creates a new ClaimConfig. -func NewClaimConfig(username, roles string) ClaimConfig { - return ClaimConfig{ - username: username, - roles: roles, - } -} - -type claims map[string]interface{} - -// Roles gets the roles from claims map. -func (c ClaimConfig) Roles(claims claims) []string { - r, ok := claims[c.roles] - if !ok { - return []string{} - } - - rls, ok := r.([]interface{}) - if ok { - roles := make([]string, 0, len(rls)) - - for _, r := range rls { - r, isString := r.(string) - if isString { - roles = append(roles, r) - } - } - - return roles - } - - roles, ok := r.([]string) - if !ok { - return []string{} - } - - return roles -} - -// Username gets the username from claims map. -func (c ClaimConfig) Username(claims claims) string { - u, ok := claims[c.username] - if !ok { - return "" - } - - username, ok := u.(string) - - if !ok { - return "" - } - - return username -} - -//nolint:gocyclo // maybe move rules to other function -func authorizeUser(ctx context.Context, rwRoles ...string) error { - fullMethod := methodNameFromContext(ctx) - if strings.HasPrefix(fullMethod, "/grpc.reflection") { - return nil - } - - u, ok := UserFromContext(ctx) - if !ok { - return status.Errorf(codes.Unauthenticated, "unauthententicated user") - } - - // following methods are only allowed when user has a rw role. - switch fullMethod { - case "/postfinance.discovery.v1.NamespaceAPI/RegisterNamespace": - if u.IsMachine() || !u.HasRole(rwRoles...) { - return status.Errorf(codes.PermissionDenied, "%s token for %s is not allowed to register a namespace", u.Kind.String(), u.Username) - } - case "/postfinance.discovery.v1.NamespaceAPI/UnregisterNamespace": - if u.IsMachine() || !u.HasRole(rwRoles...) { - return status.Errorf(codes.PermissionDenied, "%s token for %s is not allowed to unregister a namespace", u.Kind.String(), u.Username) - } - case "/postfinance.discovery.v1.ServerAPI/RegisterServer": - if u.IsMachine() || !u.HasRole(rwRoles...) { - return status.Errorf(codes.PermissionDenied, "%s token for %s is not allowed to register a server", u.Kind.String(), u.Username) - } - case "/postfinance.discovery.v1.ServerAPI/UnregisterServer": - if u.IsMachine() || !u.HasRole(rwRoles...) { - return status.Errorf(codes.PermissionDenied, "%s token for %s is not allowed to unregister a server", u.Kind.String(), u.Username) - } - case "/postfinance.discovery.v1.TokenAPI/Create": - if u.IsMachine() || !u.HasRole(rwRoles...) { - return status.Errorf(codes.PermissionDenied, "%s token for %s is not allowed to create a token", u.Kind.String(), u.Username) - } - } - - return nil -} - -func methodNameFromContext(ctx context.Context) string { - m, ok := ctx.Value(methodNameKey).(string) - if !ok { - return "" - } - - return m -} - -type key int - -const ( - userKey key = iota - methodNameKey -) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go deleted file mode 100644 index e517c2f..0000000 --- a/internal/auth/auth_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package auth - -import ( - "context" - "errors" - "testing" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - "google.golang.org/grpc/metadata" -) - -func TestFunc(t *testing.T) { - id := "username" - badTokenHandler := &TokenHandler{ - secret: "badsecret", - issuer: "wrongissuer", - } - badToken, err := badTokenHandler.Create(id, 0) - require.NoError(t, err) - tokenHandler := NewTokenHandler("thesecret", "discovery.postifnance.ch") - goodToken, err := tokenHandler.Create(id, 0) - require.NoError(t, err) - nokVerifier := mockVerifier{ok: false} - okVerifier := mockVerifier{ok: true} - claimConfig := ClaimConfig{ - username: "username", - roles: "roles", - } - - t.Run("bad machine token, nok oidc verifier", func(t *testing.T) { - f := Func(nokVerifier, tokenHandler, zap.New(nil).Sugar(), claimConfig) - m := metadata.MD{} - m.Set("authorization", "bearer "+badToken) - ctx := metadata.NewIncomingContext(context.Background(), m) - _, err = f(ctx) - assert.Error(t, err) - }) - t.Run("valid machine token, nok oidc verifier", func(t *testing.T) { - f := Func(nokVerifier, tokenHandler, zap.New(nil).Sugar(), claimConfig) - m := metadata.MD{} - m.Set("authorization", "bearer "+goodToken) - ctx := metadata.NewIncomingContext(context.Background(), m) - c, err := f(ctx) - require.NoError(t, err) - u, ok := UserFromContext(c) - require.True(t, ok) - require.False(t, u.IsUser()) - require.Equal(t, id, u.Username) - }) - t.Run("bad machine token, ok oidc verifier", func(t *testing.T) { - f := Func(okVerifier, tokenHandler, zap.New(nil).Sugar(), claimConfig) - m := metadata.MD{} - m.Set("authorization", "bearer "+badToken) - ctx := metadata.NewIncomingContext(context.Background(), m) - _, err = f(ctx) - // claims are private in oidc id token, therefore exactly this error should come - // if we could set claims here, no error should ocur. - assert.Contains(t, err.Error(), "oidc: claims not set") - }) -} - -type mockVerifier struct { - ok bool -} - -func (m mockVerifier) Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) { - if m.ok { - return &oidc.IDToken{}, nil - } - return nil, errors.New("invalid token - mock") -} diff --git a/internal/auth/client.go b/internal/auth/client.go deleted file mode 100644 index dd05f43..0000000 --- a/internal/auth/client.go +++ /dev/null @@ -1,316 +0,0 @@ -package auth - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/coreos/go-oidc/v3/oidc" - jwt "github.com/golang-jwt/jwt/v4" - "golang.org/x/oauth2" -) - -const ( - dfltTimeout = 5 * time.Second -) - -// Token holds all necessary token info. -type Token struct { - Username string `yaml:"-"` - ISS string `yaml:"-"` - AUD string `yaml:"-"` - RefreshToken string `yaml:"refresh_token"` - IDToken string `yaml:"id_token"` - AccessToken string `yaml:"access_token"` - Expiry time.Time `yaml:"-"` -} - -// ClientOption is a functional option to configure the Client. -type ClientOption func(a *Client) - -// Client handles requests to the keycloak server. -type Client struct { - cli *http.Client - endPoint string - clientID string - tokenEndpoint string -} - -// NewClient creates a new client with a configured token endpoint. -func NewClient(endPoint, clientID string, opts ...ClientOption) (*Client, error) { - c := Client{ - cli: &http.Client{ - Timeout: dfltTimeout, - }, - endPoint: endPoint, - clientID: clientID, - } - - for _, opt := range opts { - opt(&c) - } - - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", c.endPoint, ".well-known/openid-configuration"), http.NoBody) - if err != nil { - return nil, fmt.Errorf("creating token endpoint request: %w", err) - } - - resp, err := c.cli.Do(req) - if err != nil { - return nil, err - } - - defer func() { - _ = resp.Body.Close() - }() - - if err := handleResponse(resp); err != nil { - return nil, err - } - - m := map[string]interface{}{} - d := json.NewDecoder(resp.Body) - - if err := d.Decode(&m); err != nil { - return nil, fmt.Errorf("decoding body: %w", err) - } - - c.tokenEndpoint = fmt.Sprintf("%s", m["token_endpoint"]) - - return &c, nil -} - -// WithTimeout overrides the default timeout of the httpclient. -func WithTimeout(timeout time.Duration) ClientOption { - return func(c *Client) { - c.cli.Timeout = timeout - } -} - -// WithTransport overrides the default transport of the httpclient. -func WithTransport(transport http.RoundTripper) ClientOption { - return func(c *Client) { - c.cli.Transport = transport - } -} - -// Token returns an OAUTH 2.0 token with Password Grant type. -func (c *Client) Token(username, password string, out io.Writer) (*Token, error) { - t, err := c.getToken(username, password) - if err != nil { - return nil, err - } - - return t, err -} - -// Refresh refreshes a token if it expires in 10 seconds from now. -func (c *Client) Refresh(t *Token) (*Token, error) { - if t == nil { - return nil, nil - } - - if t.RefreshToken == "" { - return nil, fmt.Errorf("no refresh token provided") - } - - if !t.isExpiredIn(10 * time.Second) { - return t, nil - } - - ts, err := c.tokenSource(*t) - if err != nil { - return nil, err - } - - ot, err := ts.Token() - if err != nil { - return nil, fmt.Errorf("refreshing token: %w", err) - } - - newToken := Token{ - IDToken: ot.AccessToken, - RefreshToken: t.RefreshToken, - } - - if err := newToken.parse(); err != nil { - return t, err - } - - return &newToken, err -} - -// NewToken creates a new Token from idToken and refreshToken. -func NewToken(idToken, refreshToken string) (*Token, error) { - if idToken == "" { - return nil, fmt.Errorf("id token must not be empty") - } - - if refreshToken == "" { - return nil, fmt.Errorf("refresh token must not be empty") - } - - t := Token{ - IDToken: idToken, - RefreshToken: refreshToken, - } - if err := t.parse(); err != nil { - return nil, err - } - - return &t, nil -} - -// tokenSource returns a oauth2 TokenSource that returns a -// token until it expires, automatically refreshing it as necessary -// using the provided context. -func (c *Client) tokenSource(t Token) (oauth2.TokenSource, error) { - if t.RefreshToken == "" { - return nil, fmt.Errorf("no refresh token found") - } - - ctx := oidc.ClientContext(context.Background(), c.cli) - - provider, err := oidc.NewProvider(ctx, c.endPoint) - if err != nil { - return nil, err - } - - oauth2Config := oauth2.Config{ - ClientID: c.clientID, - Endpoint: provider.Endpoint(), - - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, - } - - ts := oauth2Config.TokenSource(ctx, &oauth2.Token{ - RefreshToken: t.RefreshToken, - }) - - return ts, nil -} - -// getToken uses the Password Grant Flow -func (c *Client) getToken(username, password string) (*Token, error) { - data := url.Values{"scope": {"openid"}, "username": {username}, "password": {password}, "grant_type": {"password"}} - - return c.requestToken(data) -} - -func (c *Client) requestToken(data url.Values) (*Token, error) { - req, err := http.NewRequest(http.MethodPost, c.tokenEndpoint, strings.NewReader(data.Encode())) - if err != nil { - return nil, fmt.Errorf("creating token request: %w", err) - } - - req.SetBasicAuth(c.clientID, "") - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := c.cli.Do(req) - if err != nil { - return nil, fmt.Errorf("requesting token: %w", err) - } - - defer func() { - _ = resp.Body.Close() - }() - - if err := handleResponse(resp); err != nil { - return nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("reading response body from token request: %w", err) - } - - m := map[string]interface{}{} - if err := json.Unmarshal(body, &m); err != nil { - return nil, fmt.Errorf("unmarshaling body: %w body: %s", err, string(body)) - } - - var t Token - - if val, ok := m["id_token"].(string); ok { - t.IDToken = val - } - - if val, ok := m["access_token"].(string); ok { - t.AccessToken = val - } - - if val, ok := m["refresh_token"].(string); ok { - t.RefreshToken = val - } - - if err := t.parse(); err != nil { - return nil, err - } - - return &t, nil -} - -// isExpiredIn returns true if token will expire in d. -func (t Token) isExpiredIn(d time.Duration) bool { - return t.Expiry.Before(time.Now().Add(d)) -} - -func (t *Token) parse() error { - var ( - parser = new(jwt.Parser) - claims = make(jwt.MapClaims) - token string - ) - - if t.IDToken == "" { - token = t.AccessToken - } else { - token = t.IDToken - } - - if _, _, err := parser.ParseUnverified(token, claims); err != nil { - return fmt.Errorf("parsing jwt token: %w", err) - } - - t.AUD = fmt.Sprintf("%s", claims["aud"]) - t.ISS = fmt.Sprintf("%s", claims["iss"]) - t.Username = fmt.Sprintf("%s", claims["username"]) - - i, ok := claims["exp"].(float64) - if !ok { - return fmt.Errorf("expiry is not of type float64: %v", claims["exp"]) - } - - t.Expiry = time.Unix(int64(i), 0) - - return nil -} - -func handleResponse(r *http.Response) error { - if r.StatusCode < 200 || r.StatusCode > 399 { - body, _ := io.ReadAll(r.Body) - - // reset the response body to the original unread state - r.Body = io.NopCloser(bytes.NewBuffer(body)) - - var e errResponse - if err := json.Unmarshal(body, &e); err != nil { - return fmt.Errorf("request failed - status %s: %s", r.Status, string(body)) - } - - return fmt.Errorf("request %s: %s: %s", r.Status, e.Error, e.ErrorDescription) - } - - return nil -} - -type errResponse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` -} diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go deleted file mode 100644 index 2ba2ec3..0000000 --- a/internal/auth/jwt.go +++ /dev/null @@ -1,94 +0,0 @@ -package auth - -import ( - "fmt" - "time" - - jwt "github.com/golang-jwt/jwt/v4" -) - -// TokenHandler creates tokens. -type TokenHandler struct { - issuer string - secret string -} - -// NewTokenHandler creates a now TokenHandler -func NewTokenHandler(secret, issuer string) *TokenHandler { - return &TokenHandler{ - issuer: issuer, - secret: secret, - } -} - -// Create creates a new token. If expires is 0, it never expires. -func (t *TokenHandler) Create(id string, expires time.Duration, namespaces ...string) (string, error) { - now := time.Now() - - claims := TokenClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - ID: id, - Issuer: t.issuer, - IssuedAt: jwt.NewNumericDate(now), - NotBefore: jwt.NewNumericDate(now), - }, - Namespaces: namespaces, - } - - if expires > 0 { - claims.ExpiresAt = jwt.NewNumericDate(now.Add(expires)) - } - - token := jwt.New(jwt.SigningMethodHS256) - token.Claims = claims - - return token.SignedString([]byte(t.secret)) -} - -// Validate validates a token. If successful it returns a machine user. -func (t *TokenHandler) Validate(token string) (*User, error) { - tknClaims := TokenClaims{} - - tkn, err := jwt.ParseWithClaims(token, &tknClaims, func(token *jwt.Token) (interface{}, error) { - return []byte(t.secret), nil - }) - - claims, ok := tkn.Claims.(*TokenClaims) - if !ok || !tkn.Valid { - return nil, fmt.Errorf("invalid token: %w", err) - } - - if claims.RegisteredClaims.Issuer != t.issuer { - return nil, fmt.Errorf("wrong issuer is '%s', not %s", tknClaims.Issuer, t.issuer) - } - - u := User{ - Username: claims.RegisteredClaims.ID, - Namespaces: claims.Namespaces, - Kind: MachineToken, - } - - if claims.ExpiresAt != nil { - u.ExpiresAt = claims.ExpiresAt.Time - } - - return &u, nil -} - -// IsMachine checks if token is a machine token issued by lslb service. -func (t *TokenHandler) IsMachine(token string) (bool, error) { - tknClaims := jwt.MapClaims{} - p := new(jwt.Parser) - - if _, _, err := p.ParseUnverified(token, &tknClaims); err != nil { - return false, err - } - - return tknClaims["iss"] == t.issuer, nil -} - -// TokenClaims is like jwt standard claims with additional list of namespaces. -type TokenClaims struct { - jwt.RegisteredClaims - Namespaces []string `json:"namespaces,omitempty"` -} diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go deleted file mode 100644 index 0627811..0000000 --- a/internal/auth/jwt_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package auth - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestJWT(t *testing.T) { - th := NewTokenHandler("thesecret", "issuer") - - token, err := th.Create("username", 1*time.Hour, "namespace1", "namespace2") - require.NoError(t, err) - assert.NotEmpty(t, token) - - fmt.Println(token) - - t.Run("valid token", func(t *testing.T) { - u, err := th.Validate(token) - require.NoError(t, err) - assert.Equal(t, u.Username, "username") - assert.Equal(t, u.Namespaces, []string{"namespace1", "namespace2"}) - assert.Equal(t, u.Kind, MachineToken) - assert.True(t, u.ExpiresAt.After(time.Now())) - }) - - t.Run("invalid token - wrong issuer", func(t *testing.T) { - oth := NewTokenHandler("thesecret", "issuer2") - u, err := oth.Validate(token) - assert.Error(t, err) - assert.Nil(t, u) - }) - - t.Run("invalid token - different secret", func(t *testing.T) { - oth := NewTokenHandler("othersecret", "issuer") - u, err := oth.Validate(token) - assert.Error(t, err) - assert.Nil(t, u) - }) -} - -func TestJWTCompatibility(t *testing.T) { - - /* - { - "jti": "username", - "iat": 1629461215.308863, - "iss": "issuer", - "nbf": 1629461215.308863, - "namespaces": [ - "namespace1", - "namespace2" - ] - } - */ - const oldToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ1c2VybmFtZSIsImlhdCI6MTYyOTQ2MTIxNS4zMDg4NjMsImlzcyI6Imlzc3VlciIsIm5iZiI6MTYyOTQ2MTIxNS4zMDg4NjMsIm5hbWVzcGFjZXMiOlsibmFtZXNwYWNlMSIsIm5hbWVzcGFjZTIiXX0.suZSwZfDuLVdAGYy3rEJE0T3sSvq-qPi9SoOizMLkas` - - /* - { - "jti": "username", - "iat": 1629461023, - "iss": "issuer", - "nbf": 1629461023, - "namespaces": [ - "namespace1", - "namespace2" - ] - } - */ - const token = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ1c2VybmFtZSIsImlhdCI6MTYyOTQ2MTAyMywiaXNzIjoiaXNzdWVyIiwibmJmIjoxNjI5NDYxMDIzLCJuYW1lc3BhY2VzIjpbIm5hbWVzcGFjZTEiLCJuYW1lc3BhY2UyIl19.MEQPTHAQNBQbn4pnqIJQctRgqnHcuJTsiHCiWmK_7ZE` - - th := NewTokenHandler("thesecret", "issuer") - - t.Run("valid old token", func(t *testing.T) { - _, err := th.Validate(oldToken) - assert.NoError(t, err) - }) - - t.Run("valid token", func(t *testing.T) { - _, err := th.Validate(token) - assert.NoError(t, err) - }) -} diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go deleted file mode 100644 index a1487c9..0000000 --- a/internal/auth/oidc.go +++ /dev/null @@ -1,73 +0,0 @@ -package auth - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "net/http" - "os" - "time" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/pkg/errors" -) - -// Verifier verifers an OIDC token. -type Verifier interface { - Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) -} - -// NewVerifier creates a new oidc verifier. -func NewVerifier(url, clientID string, timeout time.Duration, transport http.RoundTripper) (*oidc.IDTokenVerifier, error) { - cli := &http.Client{ - Timeout: timeout, - } - - if transport != nil { - cli.Transport = transport - } - - ctx := oidc.ClientContext(context.Background(), cli) - - provider, err := oidc.NewProvider(ctx, url) - if err != nil { - return nil, errors.Wrap(err, "failed to get oidc provider") - } - - oidcConfig := &oidc.Config{ - ClientID: clientID, - } - - return provider.Verifier(oidcConfig), nil -} - -// AppendCertsToSystemPool adds certificates to system cert pool. If it is not possible to get system pool, -// certificates are added to an emptycert pool. -func AppendCertsToSystemPool(pemFile string) (*x509.CertPool, error) { - caCert, err := os.ReadFile(pemFile) //nolint: gosec // we need to read that file - if err != nil { - return nil, fmt.Errorf("failed to read file '%s': %w", pemFile, err) - } - - caCertPool, err := x509.SystemCertPool() - if err != nil { - caCertPool = x509.NewCertPool() - } - - caCertPool.AppendCertsFromPEM(caCert) - - return caCertPool, nil -} - -// NewTLSTransportFromCertPool creates a new *http.Transport form cert pool. -func NewTLSTransportFromCertPool(pool *x509.CertPool) *http.Transport { - tlsConfig := &tls.Config{ - RootCAs: pool, - MinVersion: tls.VersionTLS12, - } - - return &http.Transport{ - TLSClientConfig: tlsConfig, - } -} diff --git a/internal/auth/tokenkind_string.go b/internal/auth/tokenkind_string.go deleted file mode 100644 index 6c8c022..0000000 --- a/internal/auth/tokenkind_string.go +++ /dev/null @@ -1,24 +0,0 @@ -// Code generated by "stringer -type TokenKind -linecomment"; DO NOT EDIT. - -package auth - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[MachineToken-0] - _ = x[UserToken-1] -} - -const _TokenKind_name = "machineuser" - -var _TokenKind_index = [...]uint8{0, 7, 11} - -func (i TokenKind) String() string { - if i < 0 || i >= TokenKind(len(_TokenKind_index)-1) { - return "TokenKind(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _TokenKind_name[_TokenKind_index[i]:_TokenKind_index[i+1]] -} diff --git a/internal/auth/user.go b/internal/auth/user.go deleted file mode 100644 index 63662ec..0000000 --- a/internal/auth/user.go +++ /dev/null @@ -1,76 +0,0 @@ -package auth - -import ( - "context" - "time" -) - -// User is a oicd user. -type User struct { - Username string - Email string - Roles []string - Namespaces []string - ExpiresAt time.Time - Kind TokenKind -} - -// IsUser returns true if the token corresponds to a user token and -// false if it is a machine token. -func (u User) IsUser() bool { - return u.Kind == UserToken -} - -// IsMachine returns true if the token corresponds to a machine token and -// false if it is a user token. -func (u User) IsMachine() bool { - return u.Kind == MachineToken -} - -// UserFromContext gets user from context. -func UserFromContext(ctx context.Context) (User, bool) { - userPtr, ok := ctx.Value(userKey).(*User) - if ok { - return *userPtr, true - } - - user, ok := ctx.Value(userKey).(User) - - return user, ok -} - -// HasRole returns true if user has one of roles. -func (u User) HasRole(roles ...string) bool { - for _, ur := range u.Roles { - for _, r := range roles { - if r == ur { - return true - } - } - } - - return false -} - -// HasNamespace returns true if user has one of namespaces. -func (u User) HasNamespace(namespaces ...string) bool { - for _, un := range u.Namespaces { - for _, u := range namespaces { - if u == un { - return true - } - } - } - - return false -} - -// TokenKind defines the kind of token. There are two possible tokens: machine and users. -type TokenKind int - -// Two possible tokens: machine and users. User tokens are issued by oidc provider, where machine -// tokens are issued by discovery service. -const ( - MachineToken TokenKind = iota // machine - UserToken // user -) diff --git a/internal/auth/user_test.go b/internal/auth/user_test.go deleted file mode 100644 index 9f1d44b..0000000 --- a/internal/auth/user_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package auth - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHasRoles(t *testing.T) { - var tt = []struct { - roles []string - u User - expected bool - }{ - { - []string{"a", "b"}, - User{}, - false, - }, - { - []string{"a", "b"}, - User{ - Roles: []string{"a"}, - }, - true, - }, - { - []string{"a", "b"}, - User{ - Roles: []string{"c"}, - }, - false, - }, - { - []string{}, - User{ - Roles: []string{"c"}, - }, - false, - }, - { - []string{}, - User{}, - false, - }, - } - - for _, tc := range tt { - t.Run("", func(t *testing.T) { - assert.Equal(t, tc.expected, tc.u.HasRole(tc.roles...)) - }) - } -} - -func TestHasNamespace(t *testing.T) { - var tt = []struct { - namespaces []string - u User - expected bool - }{ - { - []string{"a", "b"}, - User{}, - false, - }, - { - []string{"a", "b"}, - User{ - Namespaces: []string{"a"}, - }, - true, - }, - { - []string{"a", "b"}, - User{ - Namespaces: []string{"c"}, - }, - false, - }, - { - []string{}, - User{ - Namespaces: []string{"c"}, - }, - false, - }, - { - []string{}, - User{}, - false, - }, - } - - for _, tc := range tt { - t.Run("", func(t *testing.T) { - assert.Equal(t, tc.expected, tc.u.HasNamespace(tc.namespaces...)) - }) - } -} diff --git a/internal/authng/auth.go b/internal/authng/auth.go deleted file mode 100644 index 8137630..0000000 --- a/internal/authng/auth.go +++ /dev/null @@ -1,67 +0,0 @@ -// Package auth provides helpers to authenticate and authorize RPC calls in a Kubernetes RBAC like approach. -// JWT tokens are used to transport identity information. Tokens can either be issued by methods -// provided by this package or originate from an openid connect provider. -package auth - -import ( - "context" - "slices" -) - -const ( - // MetadataHeader is the name of the header - MetadataHeader = "authorization" - // MetadataSchema is the authorization schema - MetadataSchema = "Bearer" -) - -// Verifier verifies a JWT token. -type Verifier interface { - Verify(ctx context.Context, rawIDToken string) (*User, error) -} - -// Config describes which roles are allowed (authorized) to access certain services with the given methods. -// Example config: -// --- -// - role: reader -// rules: -// - service: postfinance.burger.namespace.v1.NamespaceAPI -// methods: -// - Get -// - Read -// - service: postfinance.burger.namespace.v1.DeploymentAPI -// methods: -// - Get -// - Read -type Config struct { - Role string `yaml:"role"` - Rules []Rule `yaml:"rules"` -} - -// Rule is an authorization rule matching the given api group and method(s). -type Rule struct { - Service string `yaml:"service"` - Methods []string `yaml:"methods"` -} - -// Configs is a slice of authorization configurations. -type Configs []Config - -// IsAuthorized returns true if the user has the permission to access the service -func (ac Configs) IsAuthorized(service, method string, user User) bool { - for idx := range ac { - authz := ac[idx] - - if slices.Contains(user.Roles, authz.Role) { - for _, rule := range authz.Rules { - if rule.Service == service { - if slices.Contains(rule.Methods, method) { - return true - } - } - } - } - } - - return false -} diff --git a/internal/authng/client.go b/internal/authng/client.go deleted file mode 100644 index 8f2d83a..0000000 --- a/internal/authng/client.go +++ /dev/null @@ -1,21 +0,0 @@ -package auth - -import ( - "context" - - "connectrpc.com/connect" -) - -// WithToken configures a token authenticator for use in connect.WithInterceptors(...). -func WithToken(token string) connect.UnaryInterceptorFunc { - return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - req.Header().Set(MetadataHeader, MetadataSchema+" "+token) - - return next(ctx, req) - }) - }) -} diff --git a/internal/authng/interceptor.go b/internal/authng/interceptor.go deleted file mode 100644 index 6200446..0000000 --- a/internal/authng/interceptor.go +++ /dev/null @@ -1,206 +0,0 @@ -package auth - -import ( - "context" - "errors" - "fmt" - "strings" - - jwt "github.com/golang-jwt/jwt/v4" - - "connectrpc.com/connect" -) - -// Authorizer can be used to authenticate and authorize ConnectRPC unary calls through the use of the provided interceptors. -type Authorizer struct { - verifiers map[string]Verifier - parser *jwt.Parser - public map[string]bool - config Configs - authCallback AuthCallback -} - -// AuthCallback is a callback function which will be executed after successful authentication. -type AuthCallback func(context.Context, User) - -// NewAuthorizer returns a configures authorizer which can be used as an interceptor to authenticate and authorize ConnectRPC -// streaming and unary calls. -func NewAuthorizer(c Configs, opts ...func(*Authorizer)) *Authorizer { - a := Authorizer{ - verifiers: make(map[string]Verifier), - parser: new(jwt.Parser), - config: c, - } - - for _, opt := range opts { - opt(&a) - } - - if len(a.verifiers) < 1 { - panic("no token verifier(s) configured, use WithVerifier() option to configure one") - } - - return &a -} - -// WithPublicEndpoints configures public endpoints. The endpoint must be fully qualified, e.g.: /postfinance.echo.v1.EchoAPI/Echo -func WithPublicEndpoints(eps ...string) func(*Authorizer) { - return func(a *Authorizer) { - public := make(map[string]bool) - for _, ep := range eps { - public[ep] = true - } - - a.public = public - } -} - -// WithAuthCallback configures a callback function in the authorizer. -func WithAuthCallback(cb AuthCallback) func(*Authorizer) { - return func(a *Authorizer) { - a.authCallback = cb - } -} - -// WithVerifier configures a token verifier for the given issuer. Can be provided multiple times with -// different issuers to validate tokens from different sources (eg. self issued and oidc issued tokens). -func WithVerifier(issuer string, verifier Verifier) func(*Authorizer) { - return func(a *Authorizer) { - a.verifiers[issuer] = verifier - } -} - -// WithVerifierByIssuerAndClientID configures a token verifier for the given issuer with different ClientIDs -// to validate tokens from the same sources with different ClientIDs. -func WithVerifierByIssuerAndClientID(issuer, clientID string, verifier Verifier) func(*Authorizer) { - return func(a *Authorizer) { - a.verifiers[fmt.Sprintf("%s::%s", issuer, clientID)] = verifier - } -} - -// UnaryServerInterceptor returns a ConnectRPC server interceptor to authenticate and authorize unary calls. -func (a *Authorizer) UnaryServerInterceptor() connect.UnaryInterceptorFunc { - interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - // public endpoint - needs no authentication or authorization - if a.public[req.Spec().Procedure] { - return next(ctx, req) - } - - // wrap authorization header in context to be compatible with grpcauth 1.2.1 - key := MetadataHeader - value := req.Header().Get(MetadataHeader) - vCtx := context.WithValue(context.Background(), key, value) //nolint:staticcheck // keep the logic from grpcauth 1.2.1 - - wrappedCtx, err := a.authenticate(vCtx) - if err != nil { - return nil, err - } - - if err := a.authorize(wrappedCtx, req.Spec().Procedure); err != nil { - return nil, err - } - - return next(wrappedCtx, req) - }) - } - - return connect.UnaryInterceptorFunc(interceptor) -} - -// Authenticate authenticates a user. The jwt token is taken out of the incoming context and the issuer (ISS) is parsed -// out of the token to determine which token verifier to call. If a verifier for the issuer is found, it will be called -// to verify the token and obtain a user object (if the token is valid). The user is then placed in the outgoing context -// and can safely be used later. -func (a *Authorizer) authenticate(ctx context.Context) (context.Context, error) { - val := ctx.Value(MetadataHeader).(string) - if val == "" { - return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing authorization header")) - } - - splits := strings.SplitN(val, " ", 2) - if len(splits) < 2 { - return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("malformed authorization string")) - } - - scheme := splits[0] - token := splits[1] - - if !strings.EqualFold(scheme, MetadataSchema) { - return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("authentication scheme %s is not supported", scheme)) - } - - var claims jwt.RegisteredClaims - // Unverified parse, since we're only interested in the issuer of the token, so we can determine which verifier we - // must use to parse and verify the token correctly. - if _, _, err := a.parser.ParseUnverified(token, &claims); err != nil { - return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("parse jwt: %w", err)) - } - - verifier := a.verifier(&claims) - if verifier == nil { - return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("unknown issuer %s and issuer with audience %s::[%s]", claims.Issuer, claims.Issuer, strings.Join(claims.Audience, ","))) - } - - user, err := verifier.Verify(ctx, token) - if err != nil { - return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("authentication failed: %w", err)) - } - - if a.authCallback != nil { - a.authCallback(ctx, *user) - } - - // warp the incoming context and put the user object into the new context - return context.WithValue(ctx, UserCtxKey, user), nil -} - -func (a *Authorizer) verifier(claims *jwt.RegisteredClaims) Verifier { - if claims.Audience == nil { - return a.verifiers[claims.Issuer] - } - - for _, audience := range claims.Audience { - for _, id := range []string{fmt.Sprintf("%s::%s", claims.Issuer, audience), claims.Issuer} { - verifier, ok := a.verifiers[id] - if ok { - return verifier - } - } - } - - return nil -} - -// Authorize authorizes a ConnectRPC call. The ConnectRPC method is splited into its group and method, the user information is -// extracted from the incoming context. With this information authorization is performed, based on the roles of a user -// and the interceptors authorization configurations. -func (a *Authorizer) authorize(ctx context.Context, fullMethod string) error { - // fullMethod in ConnectRPC is in the form /postfinance.burger.v1.NamespaceAPI/Create --> /service/method - splits := strings.SplitN(fullMethod, "/", 3) - if len(splits) != 3 { - return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("malformed ConnectRPC method %s", fullMethod)) - } - - method := splits[2] - service := splits[1] - - user, ok := UserFromContext(ctx) - if !ok { - return connect.NewError(connect.CodeFailedPrecondition, errors.New("no user information found in metadata")) - } - - if !a.config.IsAuthorized(service, method, user) { - return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("user %s with roles %s is not allowed to call %s in service %s", - user.Name, - strings.Join(user.Roles, ","), - method, - service, - )) - } - - return nil -} diff --git a/internal/authng/interceptor_test.go b/internal/authng/interceptor_test.go deleted file mode 100644 index ee28674..0000000 --- a/internal/authng/interceptor_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package auth_test - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "connectrpc.com/connect" - jwt "github.com/golang-jwt/jwt/v4" - auth "github.com/postfinance/discovery/internal/authng" - discoveryv1 "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1" - "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1/discoveryv1connect" - "github.com/stretchr/testify/require" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" -) - -const ( - testUser = "test" -) - -var ( - client *http.Client - authzConfig = auth.Configs{ - auth.Config{ - Role: "echo", - Rules: []auth.Rule{ - { - Service: discoveryv1connect.ServerAPIName, - Methods: []string{"ListServer", "RegisterServer", "UnregisterServer"}, - }, - }, - }, - } - - validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ0ZXN0IiwiaWF0IjoxNjE0NjgwOTkzLjg1NzI4MSwiaXNzIjoiZHVtbXkiLCJuYmYiOjE2MTQ2ODA5OTMuODU3MjgxLCJ1c2VyIjp7Im5hbWUiOiJ0ZXN0Iiwicm9sZXMiOlsiZWNobyJdLCJraW5kIjowfX0.Jca_DpqEwkBLSvRyJxFGd7zZcKdsNTs32nTb2TDnou0" // iss = dummy (same as the verifier), no aud, does have role "echo" - validTokenAudience = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ0ZXN0IiwiaWF0IjoxNjE0NjgwOTkzLjg1NzI4MSwiaXNzIjoiZHVtbXkiLCJuYmYiOjE2MTQ2ODA5OTMuODU3MjgxLCJhdWQiOiJ5dW1teSIsInVzZXIiOnsibmFtZSI6InRlc3QiLCJyb2xlcyI6WyJlY2hvIl0sImtpbmQiOjB9fQ.KJGgxWnZNkW_p6-2KPAMUdtKuFY8c18qbgtBa9bJDRc" // iss = dummy (same as the verifier), aud = yummy, does have role "echo" - unauthorizedToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ0ZXN0IiwiaWF0IjoxNjE0NjgwOTkzLjg1NzI4MSwiaXNzIjoiZHVtbXkiLCJuYmYiOjE2MTQ2ODA5OTMuODU3MjgxLCJ1c2VyIjp7Im5hbWUiOiJ0ZXN0Iiwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sImtpbmQiOjB9fQ.1mMuHyEGPd44coov4iTx0ijNXuCDB0KEZ2FQEMt502g" // iss = dummy (same as the verifier), no aud, does not have role "echo" - unauthorizedTokenAudience = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ0ZXN0IiwiaWF0IjoxNjE0NjgwOTkzLjg1NzI4MSwiaXNzIjoiZHVtbXkiLCJuYmYiOjE2MTQ2ODA5OTMuODU3MjgxLCJhdWQiOiJ5dW1teSIsInVzZXIiOnsibmFtZSI6InRlc3QiLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwia2luZCI6MH19.D7jvClVX3XoLhi4eXx7wac_YZsCtTPx2YqB7suHVusY" // iss = dummy (same as the verifier), aud = yummy, does not have role "echo" - invalidISSTok = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJ0ZXN0IiwiaWF0IjoxNjE0NjgwOTkzLjg1NzI4MSwiaXNzIjoiaW52YWxpZCIsIm5iZiI6MTYxNDY4MDk5My44NTcyODEsInVzZXIiOnsibmFtZSI6InRlc3QiLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwia2luZCI6MH19.A_0wNVWthu6JlPkP0JMjlYwRivEpwO0JodCNOje92Uka" // iss = invalid -) - -var _ discoveryv1connect.ServerAPIHandler = (*api)(nil) - -type api struct{} - -// ListServer implements discoveryv1connect.ServerAPIHandler. -func (a *api) ListServer(context.Context, *connect.Request[discoveryv1.ListServerRequest]) (*connect.Response[discoveryv1.ListServerResponse], error) { - resp := connect.NewResponse(&discoveryv1.ListServerResponse{ - Servers: []*discoveryv1.Server{ - { - Name: "server", - }, - }, - }) - - return resp, nil -} - -// RegisterServer implements discoveryv1connect.ServerAPIHandler. -func (a *api) RegisterServer(context.Context, *connect.Request[discoveryv1.RegisterServerRequest]) (*connect.Response[discoveryv1.RegisterServerResponse], error) { - panic("unimplemented") -} - -// UnregisterServer implements discoveryv1connect.ServerAPIHandler. -func (a *api) UnregisterServer(context.Context, *connect.Request[discoveryv1.UnregisterServerRequest]) (*connect.Response[discoveryv1.UnregisterServerResponse], error) { - panic("unimplemented") -} - -func testServer(a *auth.Authorizer, token string) (*httptest.Server, discoveryv1connect.ServerAPIClient) { - mux := http.NewServeMux() - - tfPath, tfHandler := discoveryv1connect.NewServerAPIHandler(&api{}, connect.WithInterceptors(a.UnaryServerInterceptor())) - mux.Handle(tfPath, tfHandler) - - ts := httptest.NewServer(h2c.NewHandler(mux, &http2.Server{})) - - client := discoveryv1connect.NewServerAPIClient(ts.Client(), ts.URL, connect.WithInterceptors(auth.WithToken(token))) - - return ts, client -} - -type dummyVerifier struct{} - -var _ auth.Verifier = &dummyVerifier{} - -func (d *dummyVerifier) Verify(ctx context.Context, token string) (*auth.User, error) { - p := jwt.NewParser() - - var claims auth.TokenClaims - - if _, _, err := p.ParseUnverified(token, &claims); err != nil { - return nil, err - } - - return &claims.User, nil -} - -func TestNewAuthorizer(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("observed no panic") - } - }() - - // must panic, because no verifier is configured - auth.NewAuthorizer(authzConfig) -} - -func TestAuthInterceptor(t *testing.T) { - t.Run("verifier by issuer without audience", func(t *testing.T) { - a := auth.NewAuthorizer(authzConfig, - auth.WithVerifier("dummy", &dummyVerifier{}), - auth.WithVerifierByIssuerAndClientID("dummy", testUser, nil), // would cause an error if chosen - ) - - tsOK, clientOK := testServer(a, validToken) - defer tsOK.Close() - - respOK, err := clientOK.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.NoError(t, err) - require.Len(t, respOK.Msg.GetServers(), 1) - - tsNOK, clientNOK := testServer(a, unauthorizedToken) - defer tsNOK.Close() - - respNOK, err := clientNOK.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.Error(t, err) - require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err)) - require.Nil(t, respNOK) - }) - - t.Run("verifier by issuer with audience", func(t *testing.T) { - a := auth.NewAuthorizer(authzConfig, - auth.WithVerifier("dummy", &dummyVerifier{}), - auth.WithVerifierByIssuerAndClientID("dummy", testUser, nil), // would cause an error if chosen - ) - - tsOK, clientOK := testServer(a, validTokenAudience) - defer tsOK.Close() - - respOK, err := clientOK.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.NoError(t, err) - require.Len(t, respOK.Msg.GetServers(), 1) - - tsNOK, clientNOK := testServer(a, unauthorizedTokenAudience) - defer tsNOK.Close() - - respNOK, err := clientNOK.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.Error(t, err) - require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err)) - require.Nil(t, respNOK) - }) - - t.Run("verifier by issuer and client id with audience", func(t *testing.T) { - a := auth.NewAuthorizer(authzConfig, - auth.WithVerifier("dummy", nil), // would cause an error if chosen - auth.WithVerifierByIssuerAndClientID("dummy", testUser, nil), // would cause an error if chosen - auth.WithVerifierByIssuerAndClientID("dummy", "yummy", &dummyVerifier{}), - ) - - ts, client := testServer(a, validTokenAudience) - defer ts.Close() - - resp, err := client.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.NoError(t, err) - require.Len(t, resp.Msg.GetServers(), 1) - }) - - t.Run("invalid token", func(t *testing.T) { - a := auth.NewAuthorizer(authzConfig, - auth.WithVerifier("dummy", &dummyVerifier{}), - ) - - ts, client := testServer(a, invalidISSTok) - defer ts.Close() - - resp, err := client.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.Error(t, err) - require.Equal(t, connect.CodeUnauthenticated, connect.CodeOf(err)) - require.Nil(t, resp) - }) - - t.Run("invalid token with public endpoint", func(t *testing.T) { - a := auth.NewAuthorizer(authzConfig, - auth.WithVerifier("dummy", &dummyVerifier{}), - auth.WithPublicEndpoints(discoveryv1connect.ServerAPIListServerProcedure), - ) - - ts, client := testServer(a, invalidISSTok) - defer ts.Close() - - resp, err := client.ListServer(context.TODO(), connect.NewRequest(&discoveryv1.ListServerRequest{})) - - require.NoError(t, err) - require.Len(t, resp.Msg.GetServers(), 1) - }) -} diff --git a/internal/authng/token.go b/internal/authng/token.go deleted file mode 100644 index 49e8e1d..0000000 --- a/internal/authng/token.go +++ /dev/null @@ -1,21 +0,0 @@ -package auth - -import ( - jwt "github.com/golang-jwt/jwt/v4" -) - -// TokenKind represents the two different kind of tokens. -type TokenKind int - -const ( - // SelfIssuedToken are tokens which are issued by this library. - SelfIssuedToken TokenKind = iota - // ExternalToken are tokens which are issued by an identity provider by OpenID connect. - ExternalToken -) - -// TokenClaims represents the information asserted about a subject. -type TokenClaims struct { - User User `json:"user"` - jwt.RegisteredClaims -} diff --git a/internal/authng/user.go b/internal/authng/user.go deleted file mode 100644 index 9742ada..0000000 --- a/internal/authng/user.go +++ /dev/null @@ -1,29 +0,0 @@ -package auth - -import "context" - -type contextKey int - -// UserCtxKey represents the context key -const ( - UserCtxKey contextKey = iota -) - -// User is an oidc user. -type User struct { - Name string `json:"name"` - Roles []string `json:"roles"` - Kind TokenKind `json:"kind"` -} - -// UserFromContext extracts the user information from the incoming context. -func UserFromContext(ctx context.Context) (User, bool) { - userPtr, ok := ctx.Value(UserCtxKey).(*User) - if ok { - return *userPtr, true - } - - user, ok := ctx.Value(UserCtxKey).(User) - - return user, ok -} diff --git a/internal/authng/user_test.go b/internal/authng/user_test.go deleted file mode 100644 index af74365..0000000 --- a/internal/authng/user_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package auth_test - -import ( - "context" - "testing" - - auth "github.com/postfinance/discovery/internal/authng" - "github.com/stretchr/testify/require" -) - -func TestUserFromContext(t *testing.T) { - tests := map[string]struct { - ctx context.Context - expect bool - }{ - "not found": { - ctx: context.WithValue(context.TODO(), auth.UserCtxKey, nil), - expect: false, - }, - "found": { - ctx: context.WithValue(context.TODO(), auth.UserCtxKey, auth.User{Name: "test"}), - expect: true, - }, - "found ptr": { - ctx: context.WithValue(context.TODO(), auth.UserCtxKey, &auth.User{Name: "test"}), - expect: true, - }, - } - - for name, tc := range tests { - t.Run("UserFromContext "+name, func(t *testing.T) { - r := require.New(t) - - user, found := auth.UserFromContext(tc.ctx) - r.Equal(tc.expect, found) - if tc.expect { - r.NotEmpty(user) - } else { - r.Empty(user) - } - }) - } -} diff --git a/internal/cmd/client/client.go b/internal/cmd/client/client.go index 92fb3a7..673e18a 100644 --- a/internal/cmd/client/client.go +++ b/internal/cmd/client/client.go @@ -5,21 +5,18 @@ import ( "context" "crypto/tls" "crypto/x509" + "fmt" "net/http" "os" "path/filepath" "time" + "connectrpc.com/connect" "github.com/alecthomas/kong" "github.com/hashicorp/go-cleanhttp" - "github.com/pkg/errors" - "github.com/postfinance/discovery/internal/auth" discoveryv1connect "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1/discoveryv1connect" "github.com/zbindenren/king" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" + "gitlab.pnet.ch/linux/go/crpcauth" "gopkg.in/yaml.v3" ) @@ -49,59 +46,39 @@ type Globals struct { type oidc struct { Endpoint string `help:"OIDC endpoint URL."` ClientID string `help:"OIDC client ID."` - ExternalLoginCmd string `help:"If not empty, this command is printed out for the login sub command. The command should create a id_token in token-path."` + ExternalLoginCmd string `help:"If not empty, this command is printed out for the login sub command. The command should create a id_token in token-path." required:"true"` } func (g Globals) ctx() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), g.Timeout) } -func (g Globals) conn() (*grpc.ClientConn, error) { - dialOption := grpc.WithTransportCredentials(insecure.NewCredentials()) - - if !g.Insecure { - pool, err := x509.SystemCertPool() - if err != nil { - return nil, err - } - - if g.CACert != "" { - pool, err = auth.AppendCertsToSystemPool(g.CACert) - if err != nil { - return nil, err - } - } - - creds := credentials.NewClientTLSFromCert(pool, "") - dialOption = grpc.WithTransportCredentials(creds) +func (g Globals) serverClient() (discoveryv1connect.ServerAPIClient, error) { + c, err := g.httpClient() + if err != nil { + return nil, err } - token, err := g.getToken() + token, err := g.loadToken() if err != nil { return nil, err } - dialOpts := []grpc.DialOption{dialOption, grpc.WithUnaryInterceptor(buildClientInterceptor(token))} - - return grpc.Dial(g.Address, dialOpts...) + return discoveryv1connect.NewServerAPIClient(c, g.Address, connect.WithInterceptors(crpcauth.WithToken(token))), nil } -func (g Globals) serverClient() (discoveryv1connect.ServerAPIClient, error) { +func (g Globals) serviceClient() (discoveryv1connect.ServiceAPIClient, error) { c, err := g.httpClient() if err != nil { return nil, err } - return discoveryv1connect.NewServerAPIClient(c, g.Address), nil -} - -func (g Globals) serviceClient() (discoveryv1connect.ServiceAPIClient, error) { - c, err := g.httpClient() + token, err := g.loadToken() if err != nil { return nil, err } - return discoveryv1connect.NewServiceAPIClient(c, g.Address), nil + return discoveryv1connect.NewServiceAPIClient(c, g.Address, connect.WithInterceptors(crpcauth.WithToken(token))), nil } func (g Globals) namespaceClient() (discoveryv1connect.NamespaceAPIClient, error) { @@ -110,7 +87,12 @@ func (g Globals) namespaceClient() (discoveryv1connect.NamespaceAPIClient, error return nil, err } - return discoveryv1connect.NewNamespaceAPIClient(c, g.Address), nil + token, err := g.loadToken() + if err != nil { + return nil, err + } + + return discoveryv1connect.NewNamespaceAPIClient(c, g.Address, connect.WithInterceptors(crpcauth.WithToken(token))), nil } func (g Globals) tokenClient() (discoveryv1connect.TokenAPIClient, error) { @@ -122,97 +104,35 @@ func (g Globals) tokenClient() (discoveryv1connect.TokenAPIClient, error) { return discoveryv1connect.NewTokenAPIClient(c, g.Address), nil } -func buildClientInterceptor(token string) func(context.Context, string, interface{}, interface{}, *grpc.ClientConn, grpc.UnaryInvoker, ...grpc.CallOption) error { - return func(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "bearer "+token) - - return invoker(ctx, method, req, reply, cc, opts...) - } -} - type token struct { MachineToken string `yaml:"machine_token"` // for machines RefreshToken string `yaml:"refresh_token"` IDToken string `yaml:"id_token"` } -func (g Globals) loadToken() (*token, error) { +func (g Globals) loadToken() (string, error) { path := kong.ExpandPath(g.TokenPath) d, err := os.ReadFile(filepath.Clean(path)) if err != nil { - return nil, err + return "", err } t := &token{} if err := yaml.Unmarshal(d, t); err != nil { - return nil, err + return "", err } - return t, nil -} - -func (g Globals) saveToken(t *auth.Token) error { - path := kong.ExpandPath(g.TokenPath) - dir := filepath.Dir(path) - - if err := os.MkdirAll(dir, 0o750); err != nil { - return err - } - - d, err := yaml.Marshal(t) - if err != nil { - return err - } - - return os.WriteFile(path, d, 0o600) -} - -// getToken loads or asks for user or machine token. If it is -// a user token it refreshes it if necessary. -func (g Globals) getToken() (string, error) { - var token string - - t, err := g.loadToken() - if err != nil { - return "", errors.Wrap(err, "login required") - } - - if err == nil && t.MachineToken == "" { - if t.RefreshToken != "" { - cli, err := auth.NewClient(g.OIDC.Endpoint, g.OIDC.ClientID) - if err != nil { - return "", err - } - - tkn := &auth.Token{ - RefreshToken: t.RefreshToken, - IDToken: t.IDToken, - } - - nt, err := cli.Refresh(tkn) - if err != nil { - return "", err - } - - token = nt.IDToken - - if nt.IDToken != t.IDToken { - if err := g.saveToken(nt); err != nil { - return "", err - } - } - } else { - token = t.IDToken - } + if t.MachineToken == "" && t.IDToken == "" { + return "", fmt.Errorf("no machine_token or id_token found in %s", filepath.Clean(path)) } if t.MachineToken != "" { - token = t.MachineToken + return t.MachineToken, nil } - return token, nil + return t.IDToken, nil } func (g Globals) httpClient() (*http.Client, error) { diff --git a/internal/cmd/client/login.go b/internal/cmd/client/login.go index 98d2fd1..f6772e6 100644 --- a/internal/cmd/client/login.go +++ b/internal/cmd/client/login.go @@ -3,51 +3,14 @@ package client import ( "fmt" "os" - - "github.com/postfinance/discovery/internal/auth" ) type loginCmd struct{} func (l loginCmd) Run(g *Globals) error { - if g.OIDC.ExternalLoginCmd != "" { - fmt.Fprintln(os.Stderr, "login with the following command:") - fmt.Fprintln(os.Stderr, "") - fmt.Fprint(os.Stderr, g.OIDC.ExternalLoginCmd+"\n") - - return nil - } - - user := os.Getenv("USER") - asker := auth.NewAsker(auth.WithPrompt("Enter username: "), auth.WithDfltUsername(user)) - - c, err := asker.Ask(os.Stdin, os.Stdout) - if err != nil { - return err - } - - opts := []auth.ClientOption{} - - if g.CACert != "" { - pool, err := auth.AppendCertsToSystemPool(g.CACert) - if err != nil { - return err - } - - transport := auth.NewTLSTransportFromCertPool(pool) - - opts = append(opts, auth.WithTransport(transport)) - } - - cli, err := auth.NewClient(g.OIDC.Endpoint, g.OIDC.ClientID, opts...) - if err != nil { - return err - } - - t, err := cli.Token(c.Username, c.Password, os.Stdout) - if err != nil { - return err - } + fmt.Fprintln(os.Stderr, "login with the following command:") + fmt.Fprintln(os.Stderr, "") + fmt.Fprint(os.Stderr, g.OIDC.ExternalLoginCmd+"\n") - return g.saveToken(t) + return nil } diff --git a/internal/cmd/client/token.go b/internal/cmd/client/token.go index ad5e10c..e5767dc 100644 --- a/internal/cmd/client/token.go +++ b/internal/cmd/client/token.go @@ -35,6 +35,7 @@ func (t tokenCreate) Run(g *Globals, l *zap.SugaredLogger, c *kong.Context) erro Expires: t.Expiry.String(), Id: t.ID, Namespaces: t.Namespaces, + Roles: []string{"machine"}, })) if err != nil { return err diff --git a/internal/cmd/server/rbac.go b/internal/cmd/server/rbac.go new file mode 100644 index 0000000..eac71cc --- /dev/null +++ b/internal/cmd/server/rbac.go @@ -0,0 +1,77 @@ +package server + +import ( + discoveryv1connect "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1/discoveryv1connect" + goauth "gitlab.pnet.ch/linux/go/auth" +) + +func rbacConfig() []goauth.Config { + return []goauth.Config{ + { + Role: "cop_appl_linux", + Rules: []goauth.Rule{ + { + Service: discoveryv1connect.NamespaceAPIName, + Methods: []string{ + "RegisterNamespace", + "UnregisterNamespace", + "ListNamespace", + }, + }, + { + Service: discoveryv1connect.ServerAPIName, + Methods: []string{ + "ListServer", + "UnregisterServer", + "RegisterServer", + }, + }, + { + Service: discoveryv1connect.ServiceAPIName, + Methods: []string{ + "RegisterService", + "UnRegisterService", + "ListService", + "ListTargetGroup", + }, + }, + { + Service: discoveryv1connect.TokenAPIName, + Methods: []string{ + "Create", + "Info", + }, + }, + }, + }, + { + Role: "machine", + Rules: []goauth.Rule{ + { + Service: discoveryv1connect.NamespaceAPIName, + Methods: []string{ + "RegisterNamespace", + "UnregisterNamespace", + "ListNamespace", + }, + }, + { + Service: discoveryv1connect.ServerAPIName, + Methods: []string{ + "ListServer", + "RegisterServer", + }, + }, + { + Service: discoveryv1connect.ServiceAPIName, + Methods: []string{ + "RegisterService", + "UnRegisterService", + "ListService", + "ListTargetGroup", + }, + }, + }, + }, + } +} diff --git a/internal/cmd/server/server.go b/internal/cmd/server/server.go index f00feba..b044dcd 100644 --- a/internal/cmd/server/server.go +++ b/internal/cmd/server/server.go @@ -3,18 +3,24 @@ package server import ( "context" - "net/http" + "fmt" "os" "os/signal" "regexp" "syscall" + "time" + "connectrpc.com/connect" "github.com/alecthomas/kong" - "github.com/postfinance/discovery/internal/auth" "github.com/postfinance/discovery/internal/server" + discoveryv1connect "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1/discoveryv1connect" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/zbindenren/king" + "gitlab.pnet.ch/linux/go/auth" + "gitlab.pnet.ch/linux/go/auth/oidc" + "gitlab.pnet.ch/linux/go/auth/self" + "gitlab.pnet.ch/linux/go/crpcauth" "go.uber.org/zap" ) @@ -47,7 +53,7 @@ func (s serverCmd) Run(g *Globals, l *zap.SugaredLogger, app *kong.Context, regi registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) registry.MustRegister(collectors.NewGoCollector()) - config, err := s.config(registry) + config, err := s.config(l, registry) if err != nil { return err } @@ -66,7 +72,7 @@ func (s serverCmd) Run(g *Globals, l *zap.SugaredLogger, app *kong.Context, regi ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() - srv, err := server.New(b, l, config) + srv, err := server.New(b, l, *config) if err != nil { return err } @@ -74,28 +80,42 @@ func (s serverCmd) Run(g *Globals, l *zap.SugaredLogger, app *kong.Context, regi return srv.Run(ctx) } -func (s serverCmd) config(registry prometheus.Registerer) (server.Config, error) { - var transport http.RoundTripper +func (s serverCmd) config(l *zap.SugaredLogger, registry prometheus.Registerer) (*server.Config, error) { + tokenHandler := self.NewTokenHandler(s.TokenIssuer, s.TokenSecret) - if s.CACert != "" { - pool, err := auth.AppendCertsToSystemPool(s.CACert) - if err != nil { - return server.Config{}, err + cfg := rbacConfig() + + for _, c := range cfg { + for _, r := range c.Rules { + l.Debugw("rbac", "role", c.Role, "service", r.Service, "methods", r.Methods) } + } - transport = auth.NewTLSTransportFromCertPool(pool) + v, err := oidc.NewVerifier(s.OIDC.Endpoint, s.OIDC.ClientID, 30*time.Second, &oidc.PfportalClaims{}) + if err != nil { + return nil, fmt.Errorf("setup oidc token verifier (pfportal): %w", err) } - return server.Config{ + a := crpcauth.NewAuthorizer(cfg, + crpcauth.WithVerifier("discovery.postfinance.ch", tokenHandler), + crpcauth.WithVerifierByIssuerAndClientID("https://p1-auth-oidc.pnet.ch:7048/auth/pfportal/openid", "cop", v), + crpcauth.WithAuthCallback(func(ctx context.Context, u auth.User) { + fmt.Println("-------", u.Name, u.Roles) + }), + crpcauth.WithPublicEndpoints( + discoveryv1connect.NamespaceAPIListNamespaceProcedure, + discoveryv1connect.ServerAPIListServerProcedure, + discoveryv1connect.ServiceAPIListServiceProcedure, + discoveryv1connect.TokenAPIInfoProcedure, + discoveryv1connect.TokenAPICreateProcedure, // TODO: Remove + ), + ) + + return &server.Config{ PrometheusRegistry: registry, NumReplicas: s.Replicas, ListenAddr: s.ListenAddr, - TokenIssuer: s.TokenIssuer, - TokenSecretKey: s.TokenSecret, - OIDCClient: s.OIDC.ClientID, - OIDCRoles: s.OIDC.Roles, - OIDCURL: s.OIDC.Endpoint, - ClaimConfig: auth.NewClaimConfig(s.OIDC.UsernameClaim, s.OIDC.RolesClaim), - Transport: transport, + TokenHandler: tokenHandler, + Interceptors: []connect.Interceptor{a.UnaryServerInterceptor()}, }, nil } diff --git a/internal/server/api.go b/internal/server/api.go index 33701c2..2e320c4 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -5,18 +5,20 @@ import ( "errors" "fmt" "regexp" - "strings" "time" "connectrpc.com/connect" "github.com/postfinance/discovery" - "github.com/postfinance/discovery/internal/auth" "github.com/postfinance/discovery/internal/exporter" "github.com/postfinance/discovery/internal/registry" "github.com/postfinance/discovery/internal/repo" "github.com/postfinance/discovery/internal/server/convert" + "github.com/postfinance/discovery/internal/user" discoveryv1 "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1" discoveryv1connect "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1/discoveryv1connect" + "gitlab.pnet.ch/linux/go/auth" + goauth "gitlab.pnet.ch/linux/go/auth" + "gitlab.pnet.ch/linux/go/auth/self" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -31,7 +33,7 @@ var ( // API implements the GRPC API. type API struct { r *registry.Registry - tokenHandler *auth.TokenHandler + tokenHandler *self.TokenHandler } // ListService implements discoveryv1connect.ServiceAPIHandler. @@ -91,7 +93,7 @@ func (a *API) ListTargetGroup(_ context.Context, in *connect.Request[discoveryv1 // RegisterService implements discoveryv1connect.ServiceAPIHandler. func (a *API) RegisterService(ctx context.Context, req *connect.Request[discoveryv1.RegisterServiceRequest]) (*connect.Response[discoveryv1.RegisterServiceResponse], error) { - if err := verifyUser(ctx, req.Msg.GetNamespace()); err != nil { + if err := verifyNamespace(ctx, req.Msg.GetNamespace()); err != nil { return nil, err } @@ -134,7 +136,7 @@ func (a *API) RegisterService(ctx context.Context, req *connect.Request[discover // UnRegisterService implements discoveryv1connect.ServiceAPIHandler. func (a *API) UnRegisterService(ctx context.Context, req *connect.Request[discoveryv1.UnRegisterServiceRequest]) (*connect.Response[discoveryv1.UnRegisterServiceResponse], error) { - if err := verifyUser(ctx, req.Msg.GetNamespace()); err != nil { + if err := verifyNamespace(ctx, req.Msg.GetNamespace()); err != nil { return nil, err } @@ -165,7 +167,13 @@ func (a *API) Create(_ context.Context, req *connect.Request[discoveryv1.CreateR expiry = d } - token, err := a.tokenHandler.Create(req.Msg.GetId(), expiry, req.Msg.GetNamespaces()...) + u := goauth.User{ + Name: req.Msg.GetId(), + Roles: req.Msg.GetRoles(), + Data: req.Msg.GetNamespaces(), + } + + token, err := a.tokenHandler.Create(expiry, u) if err != nil { return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to create token: %w", err)) } @@ -178,17 +186,23 @@ func (a *API) Create(_ context.Context, req *connect.Request[discoveryv1.CreateR } // Info implements discoveryv1connect.TokenAPIHandler. -func (a *API) Info(_ context.Context, in *connect.Request[discoveryv1.InfoRequest]) (*connect.Response[discoveryv1.InfoResponse], error) { - u, err := a.tokenHandler.Validate(in.Msg.GetToken()) +func (a *API) Info(ctx context.Context, in *connect.Request[discoveryv1.InfoRequest]) (*connect.Response[discoveryv1.InfoResponse], error) { + u, err := a.tokenHandler.Verify(ctx, in.Msg.GetToken()) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("token %s is not valid: %w", in.Msg.GetToken(), err)) } + ns := []string{} + + userNS := goauth.MustGetData[[]string](*u) + if userNS != nil { + ns = *userNS + } + resp := connect.NewResponse(&discoveryv1.InfoResponse{ Tokeninfo: &discoveryv1.TokenInfo{ - Id: u.Username, - Namespaces: u.Namespaces, - ExpiresAt: convert.TimeToPB(&u.ExpiresAt), + Id: u.Name, + Namespaces: ns, }, }) @@ -299,14 +313,14 @@ func (a *API) UnregisterNamespace(_ context.Context, req *connect.Request[discov return resp, nil } -func verifyUser(ctx context.Context, namespace string) error { - u, ok := auth.UserFromContext(ctx) +func verifyNamespace(ctx context.Context, namespace string) error { + u, ok := goauth.UserFromContext(ctx) if !ok { return status.Errorf(codes.Unauthenticated, "unauthententicated user") } - if u.IsMachine() && !u.HasNamespace(namespace) { - return status.Errorf(codes.PermissionDenied, "machine token %s (%s) is not allowed to change service in %s namespace", u.Username, strings.Join(u.Namespaces, ","), namespace) + if u.Kind == auth.SelfIssuedToken && !user.HasNamespace(u, namespace) { + return status.Errorf(codes.PermissionDenied, "machine token %s is not allowed to change service in %s namespace", u.Name, namespace) } return nil diff --git a/internal/server/server.go b/internal/server/server.go index 2135441..bca3cd5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,13 +10,14 @@ import ( "sync/atomic" "time" + "connectrpc.com/connect" "github.com/postfinance/discovery" - "github.com/postfinance/discovery/internal/auth" "github.com/postfinance/discovery/internal/registry" discoveryv1connect "github.com/postfinance/discovery/pkg/discoverypb/postfinance/discovery/v1/discoveryv1connect" "github.com/postfinance/store" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "gitlab.pnet.ch/linux/go/auth/self" "go.uber.org/zap" ) @@ -46,13 +47,8 @@ type Config struct { PrometheusRegistry prometheus.Registerer NumReplicas int ListenAddr string - TokenIssuer string - TokenSecretKey string - OIDCClient string - OIDCRoles []string - OIDCURL string - Transport http.RoundTripper - ClaimConfig auth.ClaimConfig + TokenHandler *self.TokenHandler + Interceptors []connect.Interceptor } // New initializes a new Server. @@ -112,13 +108,13 @@ func (s *Server) createMux(api *API) *http.ServeMux { mux.Handle("/swagger/", http.FileServer(http.FS(static))) mux.Handle("/metrics", promhttp.HandlerFor(r, promhttp.HandlerOpts{})) - httpPath, handler := discoveryv1connect.NewServerAPIHandler(api) + httpPath, handler := discoveryv1connect.NewServerAPIHandler(api, connect.WithInterceptors(s.config.Interceptors...)) mux.Handle(httpPath, handler) - httpPath, handler = discoveryv1connect.NewNamespaceAPIHandler(api) + httpPath, handler = discoveryv1connect.NewNamespaceAPIHandler(api, connect.WithInterceptors(s.config.Interceptors...)) mux.Handle(httpPath, handler) - httpPath, handler = discoveryv1connect.NewTokenAPIHandler(api) + httpPath, handler = discoveryv1connect.NewTokenAPIHandler(api, connect.WithInterceptors(s.config.Interceptors...)) mux.Handle(httpPath, handler) - httpPath, handler = discoveryv1connect.NewServiceAPIHandler(api) + httpPath, handler = discoveryv1connect.NewServiceAPIHandler(api, connect.WithInterceptors(s.config.Interceptors...)) mux.Handle(httpPath, handler) return mux @@ -127,13 +123,6 @@ func (s *Server) createMux(api *API) *http.ServeMux { func (s *Server) startHTTP(ctx context.Context) error { s.l.Infow("starting http server") - tokenHandler := auth.NewTokenHandler(s.config.TokenIssuer, s.config.TokenSecretKey) // TODO: Fix - - // verifier, err := auth.NewVerifier(s.config.OIDCURL, s.config.OIDCClient, httpClientTimeout, s.config.Transport) - // if err != nil { - // return err - // } - if err := s.config.PrometheusRegistry.Register(prometheus.NewGaugeFunc( prometheus.GaugeOpts{ Name: "discovery_replication_factor", @@ -169,7 +158,7 @@ func (s *Server) startHTTP(ctx context.Context) error { a := &API{ r: r, - tokenHandler: tokenHandler, + tokenHandler: s.config.TokenHandler, } mux := s.createMux(a) diff --git a/internal/user/user.go b/internal/user/user.go new file mode 100644 index 0000000..1d821ce --- /dev/null +++ b/internal/user/user.go @@ -0,0 +1,37 @@ +// Package user contains user logic. +package user + +import ( + "gitlab.pnet.ch/linux/go/auth" +) + +// // HasRole returns true if user has one of roles. +// func HasRole(u auth.User, roles ...string) bool { +// for _, ur := range u.Roles { +// for _, r := range roles { +// if r == ur { +// return true +// } +// } +// } +// +// return false +// } + +// HasNamespace returns true if user has one of namespaces. +func HasNamespace(u auth.User, namespaces ...string) bool { + userNamespaces := auth.MustGetData[[]string](u) + if userNamespaces == nil { + return false + } + + for _, un := range *userNamespaces { + for _, u := range namespaces { + if u == un { + return true + } + } + } + + return false +} diff --git a/internal/user/user_test.go b/internal/user/user_test.go new file mode 100644 index 0000000..32ec211 --- /dev/null +++ b/internal/user/user_test.go @@ -0,0 +1,100 @@ +package user + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.pnet.ch/linux/go/auth" +) + +// func TestHasRoles(t *testing.T) { +// tt := []struct { +// roles []string +// u auth.User +// expected bool +// }{ +// { +// []string{"a", "b"}, +// auth.User{}, +// false, +// }, +// { +// []string{"a", "b"}, +// auth.User{ +// Roles: []string{"a"}, +// }, +// true, +// }, +// { +// []string{"a", "b"}, +// auth.User{ +// Roles: []string{"c"}, +// }, +// false, +// }, +// { +// []string{}, +// auth.User{ +// Roles: []string{"c"}, +// }, +// false, +// }, +// { +// []string{}, +// auth.User{}, +// false, +// }, +// } +// +// for _, tc := range tt { +// t.Run("", func(t *testing.T) { +// assert.Equal(t, tc.expected, HasRole(tc.u, tc.roles...)) +// }) +// } +// } + +func TestHasNamespace(t *testing.T) { + tt := []struct { + namespaces []string + u auth.User + expected bool + }{ + { + []string{"a", "b"}, + auth.User{}, + false, + }, + { + []string{"a", "b"}, + auth.User{ + Data: []string{"a"}, + }, + true, + }, + { + []string{"a", "b"}, + auth.User{ + Data: []string{"c"}, + }, + false, + }, + { + []string{}, + auth.User{ + Data: []string{"c"}, + }, + false, + }, + { + []string{}, + auth.User{}, + false, + }, + } + + for _, tc := range tt { + t.Run("", func(t *testing.T) { + assert.Equal(t, tc.expected, HasNamespace(tc.u, tc.namespaces...)) + }) + } +} diff --git a/pkg/discoverypb/postfinance/discovery/v1/token_api.pb.go b/pkg/discoverypb/postfinance/discovery/v1/token_api.pb.go index 27f0d00..92e987e 100644 --- a/pkg/discoverypb/postfinance/discovery/v1/token_api.pb.go +++ b/pkg/discoverypb/postfinance/discovery/v1/token_api.pb.go @@ -30,6 +30,7 @@ type CreateRequest struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Namespaces []string `protobuf:"bytes,2,rep,name=namespaces,proto3" json:"namespaces,omitempty"` Expires string `protobuf:"bytes,3,opt,name=expires,proto3" json:"expires,omitempty"` + Roles []string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty"` } func (x *CreateRequest) Reset() { @@ -85,6 +86,13 @@ func (x *CreateRequest) GetExpires() string { return "" } +func (x *CreateRequest) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + type CreateResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -237,44 +245,45 @@ var file_postfinance_discovery_v1_token_api_proto_rawDesc = []byte{ 0x65, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x59, 0x0a, 0x0d, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6f, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x22, 0x26, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, - 0x23, 0x0a, 0x0b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x0c, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x6e, 0x66, - 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, - 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x69, 0x6e, 0x66, 0x6f, 0x32, 0xbe, 0x01, 0x0a, 0x08, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x41, 0x50, 0x49, 0x12, 0x5b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x27, - 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, - 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, - 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x55, 0x0a, 0x04, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x73, 0x74, - 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, - 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x26, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, - 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x85, 0x01, 0x0a, 0x1b, 0x63, 0x68, 0x2e, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x22, 0x26, 0x0a, + 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x23, 0x0a, 0x0b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x0c, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x09, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, + 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x6e, 0x66, 0x6f, 0x32, 0xbe, 0x01, + 0x0a, 0x08, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x50, 0x49, 0x12, 0x5b, 0x0a, 0x06, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x12, 0x27, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, - 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x42, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, - 0x70, 0x69, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x55, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, - 0x65, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x70, 0x62, 0x2f, 0x70, 0x6f, 0x73, 0x74, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x04, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x25, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, + 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, + 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, + 0x31, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x85, + 0x01, 0x0a, 0x1b, 0x63, 0x68, 0x2e, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x42, 0x0d, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x70, 0x69, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x55, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, - 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x76, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x70, + 0x62, 0x2f, 0x70, 0x6f, 0x73, 0x74, 0x66, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x64, 0x69, + 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x64, 0x69, 0x73, 0x63, 0x6f, + 0x76, 0x65, 0x72, 0x79, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/postfinance/discovery/v1/token_api.proto b/proto/postfinance/discovery/v1/token_api.proto index 4b80cfc..45400a1 100644 --- a/proto/postfinance/discovery/v1/token_api.proto +++ b/proto/postfinance/discovery/v1/token_api.proto @@ -24,6 +24,7 @@ message CreateRequest { string id = 1; repeated string namespaces = 2; string expires = 3; + repeated string roles = 4; } message CreateResponse {