diff --git a/go.mod b/go.mod index 3fa395a0b0a..d65762904fd 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,8 @@ require ( github.com/fasthttp/router v1.3.8 github.com/fsnotify/fsnotify v1.4.9 github.com/ghodss/yaml v1.0.0 - github.com/golang/protobuf v1.4.3 + github.com/gogo/protobuf v1.3.2 + github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.2.0 github.com/gorilla/mux v1.7.3 @@ -35,13 +36,16 @@ require ( github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.14.0 github.com/stretchr/testify v1.7.0 + github.com/trusch/grpc-proxy v0.0.0-20190529073533-02b64529f274 github.com/valyala/fasthttp v1.21.0 go.opencensus.io v0.22.5 go.opentelemetry.io/otel v0.19.0 go.uber.org/atomic v1.6.0 - google.golang.org/genproto v0.0.0-20201204160425-06b3db808446 - google.golang.org/grpc v1.34.0 - google.golang.org/protobuf v1.25.0 + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 + golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect + google.golang.org/genproto v0.0.0-20210524171403-669157292da3 + google.golang.org/grpc v1.38.0 + google.golang.org/protobuf v1.26.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.20.0 k8s.io/apiextensions-apiserver v0.20.0 diff --git a/go.sum b/go.sum index d21a1da1607..5e4a6dbabac 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,7 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -342,6 +343,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= @@ -518,8 +521,10 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= @@ -847,6 +852,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nacos-group/nacos-sdk-go v1.0.7/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= @@ -1113,6 +1119,8 @@ github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3/go.mod h1:QDlpd3qS71vYtakd2hmdpqhJ9nwv6mD6A30bQ1BPBFE= +github.com/trusch/grpc-proxy v0.0.0-20190529073533-02b64529f274 h1:ChAMVBRng5Dsv0rnfOxUj7vg2M9D0rafbRY1N2EEAZ8= +github.com/trusch/grpc-proxy v0.0.0-20190529073533-02b64529f274/go.mod h1:dzrPb02OTNDVimdCCBR1WAPu9a69n3VnfDyCX/GT/gE= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= @@ -1265,8 +1273,9 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -1324,8 +1333,10 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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= @@ -1413,10 +1424,15 @@ golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1425,8 +1441,10 @@ 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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= @@ -1498,8 +1516,9 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1571,8 +1590,9 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201204160425-06b3db808446 h1:65ppmIPdaZE+BO34gntwqexoTYr30IRNGmS0OGOHu3A= google.golang.org/genproto v0.0.0-20201204160425-06b3db808446/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210524171403-669157292da3 h1:xFyh6GBb+NO1L0xqb978I3sBPQpk6FrKO0jJGRvdj/0= +google.golang.org/genproto v0.0.0-20210524171403-669157292da3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1592,8 +1612,10 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1603,8 +1625,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= diff --git a/pkg/acl/acl.go b/pkg/acl/acl.go new file mode 100644 index 00000000000..9c9f4eb404a --- /dev/null +++ b/pkg/acl/acl.go @@ -0,0 +1,406 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package acl + +import ( + "context" + "encoding/asn1" + "errors" + "fmt" + "strings" + + "github.com/PuerkitoBio/purell" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + + "github.com/dapr/kit/logger" + + "github.com/dapr/dapr/pkg/config" + diag "github.com/dapr/dapr/pkg/diagnostics" + commonv1pb "github.com/dapr/dapr/pkg/proto/common/v1" +) + +var log = logger.NewLogger("dapr.acl") + +// ParseAccessControlSpec creates an in-memory copy of the Access Control Spec for fast lookup. +func ParseAccessControlSpec(accessControlSpec config.AccessControlSpec, protocol string) (*config.AccessControlList, error) { + if accessControlSpec.TrustDomain == "" && + accessControlSpec.DefaultAction == "" && + (accessControlSpec.AppPolicies == nil || len(accessControlSpec.AppPolicies) == 0) { + // No ACL has been specified + log.Debugf("No Access control policy specified") + return nil, nil + } + + var accessControlList config.AccessControlList + accessControlList.PolicySpec = make(map[string]config.AccessControlListPolicySpec) + accessControlList.DefaultAction = strings.ToLower(accessControlSpec.DefaultAction) + + accessControlList.TrustDomain = accessControlSpec.TrustDomain + if accessControlSpec.TrustDomain == "" { + accessControlList.TrustDomain = config.DefaultTrustDomain + } + + accessControlList.DefaultAction = accessControlSpec.DefaultAction + if accessControlSpec.DefaultAction == "" { + if accessControlSpec.AppPolicies == nil || len(accessControlSpec.AppPolicies) > 0 { + // Some app level policies have been specified but not default global action is set. Default to more secure option - Deny + log.Warnf("No global default action has been specified. Setting default global action as Deny") + accessControlList.DefaultAction = config.DenyAccess + } else { + // An empty ACL has been specified. Set default global action to Allow + accessControlList.DefaultAction = config.AllowAccess + } + } + + var invalidTrustDomain []string + var invalidNamespace []string + var invalidAppName bool + accessControlList.PolicySpec = make(map[string]config.AccessControlListPolicySpec) + for _, appPolicySpec := range accessControlSpec.AppPolicies { + invalid := false + if appPolicySpec.AppName == "" { + invalidAppName = true + } + if appPolicySpec.TrustDomain == "" { + invalidTrustDomain = append(invalidTrustDomain, appPolicySpec.AppName) + invalid = true + } + if appPolicySpec.Namespace == "" { + invalidNamespace = append(invalidNamespace, appPolicySpec.AppName) + invalid = true + } + + if invalid || invalidAppName { + // An invalid config was found for this app. No need to continue parsing the spec for this app + continue + } + + operationPolicy := make(map[string]config.AccessControlListOperationAction) + + // Iterate over all the operations and create a map for fast lookup + for _, appPolicy := range appPolicySpec.AppOperationActions { + // The operation name might be specified as /invoke/* + // Store the prefix as the key and use the remainder as post fix for faster lookups + // Also, prepend "/" in case it is missing in the operation name + operation := appPolicy.Operation + if !strings.HasPrefix(operation, "/") { + operation = "/" + operation + } + operationPrefix, operationPostfix := getOperationPrefixAndPostfix(operation) + + if protocol == config.HTTPProtocol { + operationPrefix = strings.ToLower(operationPrefix) + operationPostfix = strings.ToLower(operationPostfix) + } + + operationActions := config.AccessControlListOperationAction{ + OperationPostFix: operationPostfix, + VerbAction: make(map[string]string), + } + + // Iterate over all the http verbs and create a map and set the action for fast lookup + for _, verb := range appPolicy.HTTPVerb { + operationActions.VerbAction[verb] = appPolicy.Action + } + + // Store the operation action for grpc invocations where no http verb is specified + operationActions.OperationAction = appPolicy.Action + + operationPolicy[operationPrefix] = operationActions + } + aclPolicySpec := config.AccessControlListPolicySpec{ + AppName: appPolicySpec.AppName, + DefaultAction: appPolicySpec.DefaultAction, + TrustDomain: appPolicySpec.TrustDomain, + Namespace: appPolicySpec.Namespace, + AppOperationActions: operationPolicy, + } + + // The policy spec can have the same appID which belongs to different namespaces + key := getKeyForAppID(aclPolicySpec.AppName, aclPolicySpec.Namespace) + accessControlList.PolicySpec[key] = aclPolicySpec + } + + if len(invalidTrustDomain) > 0 || len(invalidNamespace) > 0 || invalidAppName { + return nil, fmt.Errorf( + "invalid access control spec. missing trustdomain for apps: %v, missing namespace for apps: %v, missing app name on at least one of the app policies: %v", + invalidTrustDomain, + invalidNamespace, + invalidAppName) + } + + return &accessControlList, nil +} + +// GetAndParseSpiffeID retrieves the SPIFFE Id from the cert and parses it. +func GetAndParseSpiffeID(ctx context.Context) (*config.SpiffeID, error) { + spiffeID, err := getSpiffeID(ctx) + if err != nil { + return nil, err + } + + id, err := parseSpiffeID(spiffeID) + return id, err +} + +func parseSpiffeID(spiffeID string) (*config.SpiffeID, error) { + if spiffeID == "" { + return nil, errors.New("input spiffe id string is empty") + } + + if !strings.HasPrefix(spiffeID, config.SpiffeIDPrefix) { + return nil, fmt.Errorf("input spiffe id: %s is invalid", spiffeID) + } + + // The SPIFFE Id will be of the format: spiffe:/// + parts := strings.Split(spiffeID, "/") + if len(parts) < 6 { + return nil, fmt.Errorf("input spiffe id: %s is invalid", spiffeID) + } + + var id config.SpiffeID + id.TrustDomain = parts[2] + id.Namespace = parts[4] + id.AppID = parts[5] + + return &id, nil +} + +func getSpiffeID(ctx context.Context) (string, error) { + var spiffeID string + peer, ok := peer.FromContext(ctx) + if ok { + if peer == nil || peer.AuthInfo == nil { + return "", errors.New("unable to retrieve peer auth info") + } + + tlsInfo := peer.AuthInfo.(credentials.TLSInfo) + + // https://www.ietf.org/rfc/rfc3280.txt + oid := asn1.ObjectIdentifier{2, 5, 29, 17} + + for _, crt := range tlsInfo.State.PeerCertificates { + for _, ext := range crt.Extensions { + if ext.Id.Equal(oid) { + var sequence asn1.RawValue + if rest, err := asn1.Unmarshal(ext.Value, &sequence); err != nil { + log.Debug(err) + continue + } else if len(rest) != 0 { + log.Debug("the SAN extension is incorrectly encoded") + continue + } + + if !sequence.IsCompound || sequence.Tag != asn1.TagSequence || sequence.Class != asn1.ClassUniversal { + log.Debug("the SAN extension is incorrectly encoded") + continue + } + + for bytes := sequence.Bytes; len(bytes) > 0; { + var rawValue asn1.RawValue + var err error + + bytes, err = asn1.Unmarshal(bytes, &rawValue) + if err != nil { + return "", err + } + + spiffeID = string(rawValue.Bytes) + if strings.HasPrefix(spiffeID, config.SpiffeIDPrefix) { + return spiffeID, nil + } + } + } + } + } + } + + return "", nil +} + +func normalizeOperation(operation string) (string, error) { + s, err := purell.NormalizeURLString(operation, purell.FlagsUsuallySafeGreedy|purell.FlagRemoveDuplicateSlashes) + if err != nil { + return "", err + } + return s, nil +} + +func ApplyAccessControlPolicies(ctx context.Context, operation string, httpVerb commonv1pb.HTTPExtension_Verb, appProtocol string, acl *config.AccessControlList) (bool, string) { + // Apply access control list filter + spiffeID, err := GetAndParseSpiffeID(ctx) + if err != nil { + // Apply the default action + log.Debugf("error while reading spiffe id from client cert: %v. applying default global policy action", err.Error()) + } + var appID, trustDomain, namespace string + if spiffeID != nil { + appID = spiffeID.AppID + namespace = spiffeID.Namespace + trustDomain = spiffeID.TrustDomain + } + + operation, err = normalizeOperation(operation) + var errMessage string + + if err != nil { + errMessage = fmt.Sprintf("error in method normalization: %s", err) + log.Debugf(errMessage) + return false, errMessage + } + + action, actionPolicy := IsOperationAllowedByAccessControlPolicy(spiffeID, appID, operation, httpVerb, appProtocol, acl) + emitACLMetrics(actionPolicy, appID, trustDomain, namespace, operation, httpVerb.String(), action) + + if !action { + errMessage = fmt.Sprintf("access control policy has denied access to appid: %s operation: %s verb: %s", appID, operation, httpVerb) + log.Debugf(errMessage) + } + + return action, errMessage +} + +func emitACLMetrics(actionPolicy, appID, trustDomain, namespace, operation, verb string, action bool) { + if action { + switch actionPolicy { + case config.ActionPolicyApp: + diag.DefaultMonitoring.RequestAllowedByAppAction(appID, trustDomain, namespace, operation, verb, action) + case config.ActionPolicyGlobal: + diag.DefaultMonitoring.RequestAllowedByGlobalAction(appID, trustDomain, namespace, operation, verb, action) + } + } else { + switch actionPolicy { + case config.ActionPolicyApp: + diag.DefaultMonitoring.RequestBlockedByAppAction(appID, trustDomain, namespace, operation, verb, action) + case config.ActionPolicyGlobal: + diag.DefaultMonitoring.RequestBlockedByGlobalAction(appID, trustDomain, namespace, operation, verb, action) + } + } +} + +// IsOperationAllowedByAccessControlPolicy determines if access control policies allow the operation on the target app. +func IsOperationAllowedByAccessControlPolicy(spiffeID *config.SpiffeID, srcAppID string, inputOperation string, httpVerb commonv1pb.HTTPExtension_Verb, appProtocol string, accessControlList *config.AccessControlList) (bool, string) { + if accessControlList == nil { + // No access control list is provided. Do nothing + return isActionAllowed(config.AllowAccess), "" + } + + action := accessControlList.DefaultAction + actionPolicy := config.ActionPolicyGlobal + + if srcAppID == "" { + // Did not receive the src app id correctly + return isActionAllowed(action), actionPolicy + } + + if spiffeID == nil { + // Could not retrieve spiffe id or it is invalid. Apply global default action + return isActionAllowed(action), actionPolicy + } + + // Look up the src app id in the in-memory table. The key is appID||namespace + key := getKeyForAppID(srcAppID, spiffeID.Namespace) + appPolicy, found := accessControlList.PolicySpec[key] + + if !found { + // no policies found for this src app id. Apply global default action + return isActionAllowed(action), actionPolicy + } + + // Match trust domain + if appPolicy.TrustDomain != spiffeID.TrustDomain { + return isActionAllowed(action), actionPolicy + } + + // Match namespace + if appPolicy.Namespace != spiffeID.Namespace { + return isActionAllowed(action), actionPolicy + } + + if appPolicy.DefaultAction != "" { + // Since the app has specified a default action, this point onwards, + // default action is the default action specified in the spec for the app + action = appPolicy.DefaultAction + actionPolicy = config.ActionPolicyApp + } + + // the in-memory table has operations stored in the format "/operation name". + // Prepend a "/" in case missing so that the match works + if !strings.HasPrefix(inputOperation, "/") { + inputOperation = "/" + inputOperation + } + + inputOperationPrefix, inputOperationPostfix := getOperationPrefixAndPostfix(inputOperation) + + // If HTTP, make case-insensitive + if appProtocol == config.HTTPProtocol { + inputOperationPrefix = strings.ToLower(inputOperationPrefix) + inputOperationPostfix = strings.ToLower(inputOperationPostfix) + } + + // The acl may specify the operation in a format /invoke/*, get and match only the prefix first + operationPolicy, found := appPolicy.AppOperationActions[inputOperationPrefix] + if found { + // The ACL might have the operation specified as /invoke/*. Here "/*" is stored as the postfix. + // Match postfix + + if strings.Contains(operationPolicy.OperationPostFix, "/*") { + if !strings.HasPrefix(inputOperationPostfix, strings.ReplaceAll(operationPolicy.OperationPostFix, "/*", "")) { + return isActionAllowed(action), actionPolicy + } + } else { + if operationPolicy.OperationPostFix != inputOperationPostfix { + return isActionAllowed(action), actionPolicy + } + } + + // Operation prefix and postfix match. Now check the operation specific policy + if appProtocol == config.HTTPProtocol { + if httpVerb != commonv1pb.HTTPExtension_NONE { + verbAction, found := operationPolicy.VerbAction[httpVerb.String()] + if found { + // An action for a specific verb is matched + action = verbAction + } else { + verbAction, found = operationPolicy.VerbAction["*"] + if found { + // The verb matched the wildcard "*" + action = verbAction + } + } + } else { + // No matching verb found in the operation specific policies. + action = appPolicy.DefaultAction + } + } else if appProtocol == config.GRPCProtocol { + // No http verb match is needed. + action = operationPolicy.OperationAction + } + } + + return isActionAllowed(action), actionPolicy +} + +func isActionAllowed(action string) bool { + return strings.EqualFold(action, config.AllowAccess) +} + +func getKeyForAppID(appID, namespace string) string { + key := appID + "||" + namespace + return key +} + +// getOperationPrefixAndPostfix returns an app operation prefix and postfix. +// The prefix can be stored in the in-memory ACL for fast lookup. +// e.g.: /invoke/*, prefix = /invoke, postfix = /*. +func getOperationPrefixAndPostfix(operation string) (string, string) { + operationParts := strings.Split(operation, "/") + operationPrefix := "/" + operationParts[1] + operationPostfix := "/" + strings.Join(operationParts[2:], "/") + + return operationPrefix, operationPostfix +} diff --git a/pkg/acl/acl_test.go b/pkg/acl/acl_test.go new file mode 100644 index 00000000000..51e21dcb60b --- /dev/null +++ b/pkg/acl/acl_test.go @@ -0,0 +1,681 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package acl + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/dapr/dapr/pkg/config" + "github.com/dapr/dapr/pkg/proto/common/v1" +) + +const ( + app1 = "app1" + app2 = "app2" + app3 = "app3" + app1Ns1 = "app1||ns1" + app2Ns2 = "app2||ns2" + app3Ns1 = "app3||ns1" + app1Ns4 = "app1||ns4" +) + +func initializeAccessControlList(protocol string) (*config.AccessControlList, error) { + inputSpec := config.AccessControlSpec{ + DefaultAction: config.DenyAccess, + TrustDomain: "abcd", + AppPolicies: []config.AppPolicySpec{ + { + AppName: app1, + DefaultAction: config.AllowAccess, + TrustDomain: "public", + Namespace: "ns1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST", "GET"}, + Operation: "/op1", + }, + { + Action: config.DenyAccess, + HTTPVerb: []string{"*"}, + Operation: "/op2", + }, + }, + }, + { + AppName: app2, + DefaultAction: config.DenyAccess, + TrustDomain: "domain1", + Namespace: "ns2", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"PUT", "GET"}, + Operation: "/op3/a/*", + }, + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST"}, + Operation: "/op4", + }, + }, + }, + { + AppName: app3, + TrustDomain: "domain1", + Namespace: "ns1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST"}, + Operation: "/op5", + }, + }, + }, + { + AppName: app1, // Duplicate app id with a different namespace + DefaultAction: config.AllowAccess, + TrustDomain: "public", + Namespace: "ns4", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"*"}, + Operation: "/op6", + }, + }, + }, + }, + } + accessControlList, err := ParseAccessControlSpec(inputSpec, protocol) + + return accessControlList, err +} + +func TestParseAccessControlSpec(t *testing.T) { + t.Run("translate to in-memory rules", func(t *testing.T) { + accessControlList, err := initializeAccessControlList(config.HTTPProtocol) + + assert.Nil(t, err) + + assert.Equal(t, config.DenyAccess, accessControlList.DefaultAction) + assert.Equal(t, "abcd", accessControlList.TrustDomain) + + // App1 + assert.Equal(t, app1, accessControlList.PolicySpec[app1Ns1].AppName) + assert.Equal(t, config.AllowAccess, accessControlList.PolicySpec[app1Ns1].DefaultAction) + assert.Equal(t, "public", accessControlList.PolicySpec[app1Ns1].TrustDomain) + assert.Equal(t, "ns1", accessControlList.PolicySpec[app1Ns1].Namespace) + + op1Actions := config.AccessControlListOperationAction{ + OperationPostFix: "/", + VerbAction: make(map[string]string), + } + op1Actions.VerbAction["POST"] = config.AllowAccess + op1Actions.VerbAction["GET"] = config.AllowAccess + op1Actions.OperationAction = config.AllowAccess + + op2Actions := config.AccessControlListOperationAction{ + OperationPostFix: "/", + VerbAction: make(map[string]string), + } + op2Actions.VerbAction["*"] = config.DenyAccess + op2Actions.OperationAction = config.DenyAccess + + assert.Equal(t, 2, len(accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op1"].VerbAction)) + assert.Equal(t, op1Actions, accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op1"]) + assert.Equal(t, 1, len(accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op2"].VerbAction)) + assert.Equal(t, op2Actions, accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op2"]) + + // App2 + assert.Equal(t, app2, accessControlList.PolicySpec[app2Ns2].AppName) + assert.Equal(t, config.DenyAccess, accessControlList.PolicySpec[app2Ns2].DefaultAction) + assert.Equal(t, "domain1", accessControlList.PolicySpec[app2Ns2].TrustDomain) + assert.Equal(t, "ns2", accessControlList.PolicySpec[app2Ns2].Namespace) + + op3Actions := config.AccessControlListOperationAction{ + OperationPostFix: "/a/*", + VerbAction: make(map[string]string), + } + op3Actions.VerbAction["PUT"] = config.AllowAccess + op3Actions.VerbAction["GET"] = config.AllowAccess + op3Actions.OperationAction = config.AllowAccess + + op4Actions := config.AccessControlListOperationAction{ + OperationPostFix: "/", + VerbAction: make(map[string]string), + } + op4Actions.VerbAction["POST"] = config.AllowAccess + op4Actions.OperationAction = config.AllowAccess + + assert.Equal(t, 2, len(accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op3"].VerbAction)) + assert.Equal(t, op3Actions, accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op3"]) + assert.Equal(t, 1, len(accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op4"].VerbAction)) + assert.Equal(t, op4Actions, accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op4"]) + + // App3 + assert.Equal(t, app3, accessControlList.PolicySpec[app3Ns1].AppName) + assert.Equal(t, "", accessControlList.PolicySpec[app3Ns1].DefaultAction) + assert.Equal(t, "domain1", accessControlList.PolicySpec[app3Ns1].TrustDomain) + assert.Equal(t, "ns1", accessControlList.PolicySpec[app3Ns1].Namespace) + + op5Actions := config.AccessControlListOperationAction{ + OperationPostFix: "/", + VerbAction: make(map[string]string), + } + op5Actions.VerbAction["POST"] = config.AllowAccess + op5Actions.OperationAction = config.AllowAccess + + assert.Equal(t, 1, len(accessControlList.PolicySpec[app3Ns1].AppOperationActions["/op5"].VerbAction)) + assert.Equal(t, op5Actions, accessControlList.PolicySpec[app3Ns1].AppOperationActions["/op5"]) + + // App1 with a different namespace + assert.Equal(t, app1, accessControlList.PolicySpec[app1Ns4].AppName) + assert.Equal(t, config.AllowAccess, accessControlList.PolicySpec[app1Ns4].DefaultAction) + assert.Equal(t, "public", accessControlList.PolicySpec[app1Ns4].TrustDomain) + assert.Equal(t, "ns4", accessControlList.PolicySpec[app1Ns4].Namespace) + + op6Actions := config.AccessControlListOperationAction{ + OperationPostFix: "/", + VerbAction: make(map[string]string), + } + op6Actions.VerbAction["*"] = config.AllowAccess + op6Actions.OperationAction = config.AllowAccess + + assert.Equal(t, 1, len(accessControlList.PolicySpec[app1Ns4].AppOperationActions["/op6"].VerbAction)) + assert.Equal(t, op6Actions, accessControlList.PolicySpec[app1Ns4].AppOperationActions["/op6"]) + }) + + t.Run("test when no trust domain and namespace specified in app policy", func(t *testing.T) { + invalidAccessControlSpec := config.AccessControlSpec{ + DefaultAction: config.DenyAccess, + TrustDomain: "public", + AppPolicies: []config.AppPolicySpec{ + { + AppName: app1, + DefaultAction: config.AllowAccess, + Namespace: "ns1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST", "GET"}, + Operation: "/op1", + }, + { + Action: config.DenyAccess, + HTTPVerb: []string{"*"}, + Operation: "/op2", + }, + }, + }, + { + AppName: app2, + DefaultAction: config.DenyAccess, + TrustDomain: "domain1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"PUT", "GET"}, + Operation: "/op3/a/*", + }, + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST"}, + Operation: "/op4", + }, + }, + }, + { + AppName: "", + DefaultAction: config.DenyAccess, + TrustDomain: "domain1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"PUT", "GET"}, + Operation: "/op3/a/*", + }, + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST"}, + Operation: "/op4", + }, + }, + }, + }, + } + + _, err := ParseAccessControlSpec(invalidAccessControlSpec, "http") + assert.Error(t, err, "invalid access control spec. missing trustdomain for apps: [%s], missing namespace for apps: [%s], missing app name on at least one of the app policies: true", app1, app2) + }) + + t.Run("test when no trust domain is specified for the app", func(t *testing.T) { + accessControlSpec := config.AccessControlSpec{ + DefaultAction: config.DenyAccess, + TrustDomain: "", + AppPolicies: []config.AppPolicySpec{ + { + AppName: app1, + DefaultAction: config.AllowAccess, + TrustDomain: "public", + Namespace: "ns1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST", "GET"}, + Operation: "/op1", + }, + { + Action: config.DenyAccess, + HTTPVerb: []string{"*"}, + Operation: "/op2", + }, + }, + }, + }, + } + + accessControlList, _ := ParseAccessControlSpec(accessControlSpec, "http") + assert.Equal(t, "public", accessControlList.PolicySpec[app1Ns1].TrustDomain) + }) + + t.Run("test when no access control policy has been specified", func(t *testing.T) { + invalidAccessControlSpec := config.AccessControlSpec{ + DefaultAction: "", + TrustDomain: "", + AppPolicies: []config.AppPolicySpec{}, + } + + accessControlList, _ := ParseAccessControlSpec(invalidAccessControlSpec, "http") + assert.Nil(t, accessControlList) + }) + + t.Run("test when no default global action has been specified", func(t *testing.T) { + invalidAccessControlSpec := config.AccessControlSpec{ + TrustDomain: "public", + AppPolicies: []config.AppPolicySpec{ + { + AppName: app1, + DefaultAction: config.AllowAccess, + TrustDomain: "domain1", + Namespace: "ns1", + AppOperationActions: []config.AppOperation{ + { + Action: config.AllowAccess, + HTTPVerb: []string{"POST", "GET"}, + Operation: "/op1", + }, + { + Action: config.DenyAccess, + HTTPVerb: []string{"*"}, + Operation: "/op2", + }, + }, + }, + }, + } + + accessControlList, _ := ParseAccessControlSpec(invalidAccessControlSpec, "http") + assert.Equal(t, accessControlList.DefaultAction, config.DenyAccess) + }) +} + +func TestSpiffeID(t *testing.T) { + t.Run("test parse spiffe id", func(t *testing.T) { + spiffeID := "spiffe://mydomain/ns/mynamespace/myappid" + id, err := parseSpiffeID(spiffeID) + assert.Equal(t, "mydomain", id.TrustDomain) + assert.Equal(t, "mynamespace", id.Namespace) + assert.Equal(t, "myappid", id.AppID) + assert.Nil(t, err) + }) + + t.Run("test parse invalid spiffe id", func(t *testing.T) { + spiffeID := "abcd" + _, err := parseSpiffeID(spiffeID) + assert.NotNil(t, err) + }) + + t.Run("test parse spiffe id with not all fields", func(t *testing.T) { + spiffeID := "spiffe://mydomain/ns/myappid" + _, err := parseSpiffeID(spiffeID) + assert.NotNil(t, err) + }) + + t.Run("test empty spiffe id", func(t *testing.T) { + spiffeID := "" + _, err := parseSpiffeID(spiffeID) + assert.NotNil(t, err) + }) +} + +func TestIsOperationAllowedByAccessControlPolicy(t *testing.T) { + t.Run("test when no acl specified", func(t *testing.T) { + srcAppID := app1 + spiffeID := config.SpiffeID{ + TrustDomain: "public", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, config.HTTPProtocol, nil) + // Action = Allow the operation since no ACL is defined + assert.True(t, isAllowed) + }) + + t.Run("test when no matching app in acl found", func(t *testing.T) { + srcAppID := "appX" + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "public", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Default global action + assert.False(t, isAllowed) + }) + + t.Run("test when trust domain does not match", func(t *testing.T) { + srcAppID := app1 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "private", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Ignore policy and apply global default action + assert.False(t, isAllowed) + }) + + t.Run("test when namespace does not match", func(t *testing.T) { + srcAppID := app1 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "public", + Namespace: "abcd", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Ignore policy and apply global default action + assert.False(t, isAllowed) + }) + + t.Run("test when spiffe id is nil", func(t *testing.T) { + srcAppID := app1 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(nil, srcAppID, "op1", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Default global action + assert.False(t, isAllowed) + }) + + t.Run("test when src app id is empty", func(t *testing.T) { + srcAppID := "" + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(nil, srcAppID, "op1", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Default global action + assert.False(t, isAllowed) + }) + + t.Run("test when operation is not found in the policy spec", func(t *testing.T) { + srcAppID := app1 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "public", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "opX", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Ignore policy and apply default action for app + assert.True(t, isAllowed) + }) + + t.Run("test http case-sensitivity when matching operation post fix", func(t *testing.T) { + srcAppID := app1 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "public", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "Op2", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Ignore policy and apply default action for app + assert.False(t, isAllowed) + }) + + t.Run("test when http verb is not found", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_PUT, config.HTTPProtocol, accessControlList) + // Action = Default action for the specific app + assert.False(t, isAllowed) + }) + + t.Run("test when default action for app is not specified and no matching http verb found", func(t *testing.T) { + srcAppID := app3 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op5", common.HTTPExtension_PUT, config.HTTPProtocol, accessControlList) + // Action = Global Default action + assert.False(t, isAllowed) + }) + + t.Run("test when http verb matches *", func(t *testing.T) { + srcAppID := app1 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "public", + Namespace: "ns1", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op2", common.HTTPExtension_PUT, config.HTTPProtocol, accessControlList) + // Action = Default action for the specific verb + assert.False(t, isAllowed) + }) + + t.Run("test when http verb matches a specific verb", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Default action for the specific verb + assert.True(t, isAllowed) + }) + + t.Run("test when operation is invoked with /", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op4", common.HTTPExtension_POST, config.HTTPProtocol, accessControlList) + // Action = Default action for the specific verb + assert.True(t, isAllowed) + }) + + t.Run("test when http verb is not specified", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_NONE, config.HTTPProtocol, accessControlList) + // Action = Default action for the app + assert.False(t, isAllowed) + }) + + t.Run("test when matching operation post fix is specified in policy spec", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op3/a", common.HTTPExtension_PUT, config.HTTPProtocol, accessControlList) + // Action = Default action for the specific verb + assert.True(t, isAllowed) + }) + + t.Run("test grpc case-sensitivity when matching operation post fix", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.GRPCProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/OP4", common.HTTPExtension_NONE, config.GRPCProtocol, accessControlList) + // Action = Default action for the specific verb + assert.False(t, isAllowed) + }) + + t.Run("test when non-matching operation post fix is specified in policy spec", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op3/b/b", common.HTTPExtension_PUT, config.HTTPProtocol, accessControlList) + // Action = Default action for the app + assert.False(t, isAllowed) + }) + + t.Run("test when non-matching operation post fix is specified in policy spec", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.HTTPProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op3/a/b", common.HTTPExtension_PUT, config.HTTPProtocol, accessControlList) + // Action = Default action for the app + assert.True(t, isAllowed) + }) + + t.Run("test with grpc invocation", func(t *testing.T) { + srcAppID := app2 + accessControlList, _ := initializeAccessControlList(config.GRPCProtocol) + spiffeID := config.SpiffeID{ + TrustDomain: "domain1", + Namespace: "ns2", + AppID: srcAppID, + } + isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_NONE, config.GRPCProtocol, accessControlList) + // Action = Default action for the app + assert.True(t, isAllowed) + }) +} + +func TestGetOperationPrefixAndPostfix(t *testing.T) { + t.Run("test when operation single post fix exists", func(t *testing.T) { + operation := "/invoke/*" + prefix, postfix := getOperationPrefixAndPostfix(operation) + assert.Equal(t, "/invoke", prefix) + assert.Equal(t, "/*", postfix) + }) + + t.Run("test when operation longer post fix exists", func(t *testing.T) { + operation := "/invoke/a/*" + prefix, postfix := getOperationPrefixAndPostfix(operation) + assert.Equal(t, "/invoke", prefix) + assert.Equal(t, "/a/*", postfix) + }) + + t.Run("test when operation no post fix exists", func(t *testing.T) { + operation := "/invoke" + prefix, postfix := getOperationPrefixAndPostfix(operation) + assert.Equal(t, "/invoke", prefix) + assert.Equal(t, "/", postfix) + }) + + t.Run("test operation multi path post fix exists", func(t *testing.T) { + operation := "/invoke/a/b/*" + prefix, postfix := getOperationPrefixAndPostfix(operation) + assert.Equal(t, "/invoke", prefix) + assert.Equal(t, "/a/b/*", postfix) + }) +} + +func TestNormalizeOperation(t *testing.T) { + t.Run("normal path no slash", func(t *testing.T) { + p := "path" + p, _ = normalizeOperation(p) + + assert.Equal(t, "path", p) + }) + + t.Run("normal path caps", func(t *testing.T) { + p := "Path" + p, _ = normalizeOperation(p) + + assert.Equal(t, "Path", p) + }) + + t.Run("single slash", func(t *testing.T) { + p := "/path" + p, _ = normalizeOperation(p) + + assert.Equal(t, "/path", p) + }) + + t.Run("multiple slashes", func(t *testing.T) { + p := "///path" + p, _ = normalizeOperation(p) + + assert.Equal(t, "/path", p) + }) + + t.Run("prefix", func(t *testing.T) { + p := "../path" + p, _ = normalizeOperation(p) + + assert.Equal(t, "path", p) + }) + + t.Run("encoded", func(t *testing.T) { + p := "path%72" + p, _ = normalizeOperation(p) + + assert.Equal(t, "pathr", p) + }) + + t.Run("normal multiple paths", func(t *testing.T) { + p := "path1/path2/path3" + p, _ = normalizeOperation(p) + + assert.Equal(t, "path1/path2/path3", p) + }) + + t.Run("normal multiple paths leading slash", func(t *testing.T) { + p := "/path1/path2/path3" + p, _ = normalizeOperation(p) + + assert.Equal(t, "/path1/path2/path3", p) + }) +} diff --git a/pkg/actors/actors.go b/pkg/actors/actors.go index ad3067fd112..b8db80657de 100644 --- a/pkg/actors/actors.go +++ b/pkg/actors/actors.go @@ -68,7 +68,7 @@ type actorsRuntime struct { store state.Store transactionalStore state.TransactionalStore placement *internal.ActorPlacement - grpcConnectionFn func(address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool) (*grpc.ClientConn, error) + grpcConnectionFn func(ctx context.Context, address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool, customOpts ...grpc.DialOption) (*grpc.ClientConn, error) config Config actorsTable *sync.Map activeTimers *sync.Map @@ -100,7 +100,7 @@ const ( func NewActors( stateStore state.Store, appChannel channel.AppChannel, - grpcConnectionFn func(address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool) (*grpc.ClientConn, error), + grpcConnectionFn func(ctx context.Context, address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool, customOpts ...grpc.DialOption) (*grpc.ClientConn, error), config Config, certChain *dapr_credentials.CertChain, tracingSpec configuration.TracingSpec, @@ -304,7 +304,7 @@ func (a *actorsRuntime) callRemoteActorWithRetry( code := status.Code(err) if code == codes.Unavailable || code == codes.Unauthenticated { - _, err = a.grpcConnectionFn(targetAddress, targetID, a.config.Namespace, false, true, false) + _, err = a.grpcConnectionFn(context.TODO(), targetAddress, targetID, a.config.Namespace, false, true, false) if err != nil { return nil, err } @@ -378,7 +378,7 @@ func (a *actorsRuntime) callRemoteActor( ctx context.Context, targetAddress, targetID string, req *invokev1.InvokeMethodRequest) (*invokev1.InvokeMethodResponse, error) { - conn, err := a.grpcConnectionFn(targetAddress, targetID, a.config.Namespace, false, false, false) + conn, err := a.grpcConnectionFn(context.TODO(), targetAddress, targetID, a.config.Namespace, false, false, false) if err != nil { return nil, err } diff --git a/pkg/config/configuration.go b/pkg/config/configuration.go index a6c78edb3f6..ac23df725eb 100644 --- a/pkg/config/configuration.go +++ b/pkg/config/configuration.go @@ -7,9 +7,7 @@ package config import ( "context" - "encoding/asn1" "encoding/json" - "fmt" "io/ioutil" "os" "sort" @@ -18,20 +16,13 @@ import ( grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" "github.com/pkg/errors" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/peer" yaml "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" - "github.com/dapr/kit/logger" - - "github.com/dapr/dapr/pkg/proto/common/v1" operatorv1pb "github.com/dapr/dapr/pkg/proto/operator/v1" ) -var log = logger.NewLogger("dapr.configuration") - const ( operatorCallTimeout = time.Second * 5 operatorMaxRetries = 100 @@ -328,326 +319,6 @@ func containsKey(s []string, key string) bool { return index < len(s) && s[index] == key } -// ParseAccessControlSpec creates an in-memory copy of the Access Control Spec for fast lookup. -func ParseAccessControlSpec(accessControlSpec AccessControlSpec, protocol string) (*AccessControlList, error) { - if accessControlSpec.TrustDomain == "" && - accessControlSpec.DefaultAction == "" && - (accessControlSpec.AppPolicies == nil || len(accessControlSpec.AppPolicies) == 0) { - // No ACL has been specified - log.Debugf("No Access control policy specified") - return nil, nil - } - - var accessControlList AccessControlList - accessControlList.PolicySpec = make(map[string]AccessControlListPolicySpec) - accessControlList.DefaultAction = strings.ToLower(accessControlSpec.DefaultAction) - - accessControlList.TrustDomain = accessControlSpec.TrustDomain - if accessControlSpec.TrustDomain == "" { - accessControlList.TrustDomain = DefaultTrustDomain - } - - accessControlList.DefaultAction = accessControlSpec.DefaultAction - if accessControlSpec.DefaultAction == "" { - if accessControlSpec.AppPolicies == nil || len(accessControlSpec.AppPolicies) > 0 { - // Some app level policies have been specified but not default global action is set. Default to more secure option - Deny - log.Warnf("No global default action has been specified. Setting default global action as Deny") - accessControlList.DefaultAction = DenyAccess - } else { - // An empty ACL has been specified. Set default global action to Allow - accessControlList.DefaultAction = AllowAccess - } - } - - var invalidTrustDomain []string - var invalidNamespace []string - var invalidAppName bool - accessControlList.PolicySpec = make(map[string]AccessControlListPolicySpec) - for _, appPolicySpec := range accessControlSpec.AppPolicies { - invalid := false - if appPolicySpec.AppName == "" { - invalidAppName = true - } - if appPolicySpec.TrustDomain == "" { - invalidTrustDomain = append(invalidTrustDomain, appPolicySpec.AppName) - invalid = true - } - if appPolicySpec.Namespace == "" { - invalidNamespace = append(invalidNamespace, appPolicySpec.AppName) - invalid = true - } - - if invalid || invalidAppName { - // An invalid config was found for this app. No need to continue parsing the spec for this app - continue - } - - operationPolicy := make(map[string]AccessControlListOperationAction) - - // Iterate over all the operations and create a map for fast lookup - for _, appPolicy := range appPolicySpec.AppOperationActions { - // The operation name might be specified as /invoke/* - // Store the prefix as the key and use the remainder as post fix for faster lookups - // Also, prepend "/" in case it is missing in the operation name - operation := appPolicy.Operation - if !strings.HasPrefix(operation, "/") { - operation = "/" + operation - } - operationPrefix, operationPostfix := getOperationPrefixAndPostfix(operation) - - if protocol == HTTPProtocol { - operationPrefix = strings.ToLower(operationPrefix) - operationPostfix = strings.ToLower(operationPostfix) - } - - operationActions := AccessControlListOperationAction{ - OperationPostFix: operationPostfix, - VerbAction: make(map[string]string), - } - - // Iterate over all the http verbs and create a map and set the action for fast lookup - for _, verb := range appPolicy.HTTPVerb { - operationActions.VerbAction[verb] = appPolicy.Action - } - - // Store the operation action for grpc invocations where no http verb is specified - operationActions.OperationAction = appPolicy.Action - - operationPolicy[operationPrefix] = operationActions - } - aclPolicySpec := AccessControlListPolicySpec{ - AppName: appPolicySpec.AppName, - DefaultAction: appPolicySpec.DefaultAction, - TrustDomain: appPolicySpec.TrustDomain, - Namespace: appPolicySpec.Namespace, - AppOperationActions: operationPolicy, - } - - // The policy spec can have the same appID which belongs to different namespaces - key := getKeyForAppID(aclPolicySpec.AppName, aclPolicySpec.Namespace) - accessControlList.PolicySpec[key] = aclPolicySpec - } - - if len(invalidTrustDomain) > 0 || len(invalidNamespace) > 0 || invalidAppName { - return nil, errors.New(fmt.Sprintf( - "invalid access control spec. missing trustdomain for apps: %v, missing namespace for apps: %v, missing app name on at least one of the app policies: %v", - invalidTrustDomain, - invalidNamespace, - invalidAppName)) - } - - return &accessControlList, nil -} - -// GetAndParseSpiffeID retrieves the SPIFFE Id from the cert and parses it. -func GetAndParseSpiffeID(ctx context.Context) (*SpiffeID, error) { - spiffeID, err := getSpiffeID(ctx) - if err != nil { - return nil, err - } - - id, err := parseSpiffeID(spiffeID) - return id, err -} - -func parseSpiffeID(spiffeID string) (*SpiffeID, error) { - if spiffeID == "" { - return nil, errors.New("input spiffe id string is empty") - } - - if !strings.HasPrefix(spiffeID, SpiffeIDPrefix) { - return nil, errors.New(fmt.Sprintf("input spiffe id: %s is invalid", spiffeID)) - } - - // The SPIFFE Id will be of the format: spiffe:/// - parts := strings.Split(spiffeID, "/") - if len(parts) < 6 { - return nil, errors.New(fmt.Sprintf("input spiffe id: %s is invalid", spiffeID)) - } - - var id SpiffeID - id.TrustDomain = parts[2] - id.Namespace = parts[4] - id.AppID = parts[5] - - return &id, nil -} - -func getSpiffeID(ctx context.Context) (string, error) { - var spiffeID string - peer, ok := peer.FromContext(ctx) - if ok { - if peer == nil || peer.AuthInfo == nil { - return "", errors.New("unable to retrieve peer auth info") - } - - tlsInfo := peer.AuthInfo.(credentials.TLSInfo) - - // https://www.ietf.org/rfc/rfc3280.txt - oid := asn1.ObjectIdentifier{2, 5, 29, 17} - - for _, crt := range tlsInfo.State.PeerCertificates { - for _, ext := range crt.Extensions { - if ext.Id.Equal(oid) { - var sequence asn1.RawValue - if rest, err := asn1.Unmarshal(ext.Value, &sequence); err != nil { - log.Debug(err) - continue - } else if len(rest) != 0 { - log.Debug("the SAN extension is incorrectly encoded") - continue - } - - if !sequence.IsCompound || sequence.Tag != asn1.TagSequence || sequence.Class != asn1.ClassUniversal { - log.Debug("the SAN extension is incorrectly encoded") - continue - } - - for bytes := sequence.Bytes; len(bytes) > 0; { - var rawValue asn1.RawValue - var err error - - bytes, err = asn1.Unmarshal(bytes, &rawValue) - if err != nil { - return "", err - } - - spiffeID = string(rawValue.Bytes) - if strings.HasPrefix(spiffeID, SpiffeIDPrefix) { - return spiffeID, nil - } - } - } - } - } - } - - return "", nil -} - -// IsOperationAllowedByAccessControlPolicy determines if access control policies allow the operation on the target app. -func IsOperationAllowedByAccessControlPolicy(spiffeID *SpiffeID, srcAppID string, inputOperation string, httpVerb common.HTTPExtension_Verb, appProtocol string, accessControlList *AccessControlList) (bool, string) { - if accessControlList == nil { - // No access control list is provided. Do nothing - return isActionAllowed(AllowAccess), "" - } - - action := accessControlList.DefaultAction - actionPolicy := ActionPolicyGlobal - - if srcAppID == "" { - // Did not receive the src app id correctly - return isActionAllowed(action), actionPolicy - } - - if spiffeID == nil { - // Could not retrieve spiffe id or it is invalid. Apply global default action - return isActionAllowed(action), actionPolicy - } - - // Look up the src app id in the in-memory table. The key is appID||namespace - key := getKeyForAppID(srcAppID, spiffeID.Namespace) - appPolicy, found := accessControlList.PolicySpec[key] - - if !found { - // no policies found for this src app id. Apply global default action - return isActionAllowed(action), actionPolicy - } - - // Match trust domain - if appPolicy.TrustDomain != spiffeID.TrustDomain { - return isActionAllowed(action), actionPolicy - } - - // Match namespace - if appPolicy.Namespace != spiffeID.Namespace { - return isActionAllowed(action), actionPolicy - } - - if appPolicy.DefaultAction != "" { - // Since the app has specified a default action, this point onwards, - // default action is the default action specified in the spec for the app - action = appPolicy.DefaultAction - actionPolicy = ActionPolicyApp - } - - // the in-memory table has operations stored in the format "/operation name". - // Prepend a "/" in case missing so that the match works - if !strings.HasPrefix(inputOperation, "/") { - inputOperation = "/" + inputOperation - } - - inputOperationPrefix, inputOperationPostfix := getOperationPrefixAndPostfix(inputOperation) - - // If HTTP, make case-insensitive - if appProtocol == HTTPProtocol { - inputOperationPrefix = strings.ToLower(inputOperationPrefix) - inputOperationPostfix = strings.ToLower(inputOperationPostfix) - } - - // The acl may specify the operation in a format /invoke/*, get and match only the prefix first - operationPolicy, found := appPolicy.AppOperationActions[inputOperationPrefix] - if found { - // The ACL might have the operation specified as /invoke/*. Here "/*" is stored as the postfix. - // Match postfix - - if strings.Contains(operationPolicy.OperationPostFix, "/*") { - if !strings.HasPrefix(inputOperationPostfix, strings.ReplaceAll(operationPolicy.OperationPostFix, "/*", "")) { - return isActionAllowed(action), actionPolicy - } - } else { - if operationPolicy.OperationPostFix != inputOperationPostfix { - return isActionAllowed(action), actionPolicy - } - } - - // Operation prefix and postfix match. Now check the operation specific policy - if appProtocol == HTTPProtocol { - if httpVerb != common.HTTPExtension_NONE { - verbAction, found := operationPolicy.VerbAction[httpVerb.String()] - if found { - // An action for a specific verb is matched - action = verbAction - } else { - verbAction, found = operationPolicy.VerbAction["*"] - if found { - // The verb matched the wildcard "*" - action = verbAction - } - } - } else { - // No matching verb found in the operation specific policies. - action = appPolicy.DefaultAction - } - } else if appProtocol == GRPCProtocol { - // No http verb match is needed. - action = operationPolicy.OperationAction - } - } - - return isActionAllowed(action), actionPolicy -} - -func isActionAllowed(action string) bool { - return strings.EqualFold(action, AllowAccess) -} - -func getKeyForAppID(appID, namespace string) string { - key := appID + "||" + namespace - return key -} - -// getOperationPrefixAndPostfix returns an app operation prefix and postfix -// The prefix can be stored in the in-memory ACL for fast lookup -// e.g.: /invoke/*, prefix = /invoke, postfix = /*. -func getOperationPrefixAndPostfix(operation string) (string, string) { - operationParts := strings.Split(operation, "/") - operationPrefix := "/" + operationParts[1] - operationPostfix := "/" + strings.Join(operationParts[2:], "/") - - return operationPrefix, operationPostfix -} - func IsFeatureEnabled(features []FeatureSpec, target Feature) bool { for _, feature := range features { if feature.Name == target { diff --git a/pkg/config/configuration_test.go b/pkg/config/configuration_test.go index 287651f8f93..e0d7a3549e6 100644 --- a/pkg/config/configuration_test.go +++ b/pkg/config/configuration_test.go @@ -11,18 +11,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - - "github.com/dapr/dapr/pkg/proto/common/v1" -) - -const ( - app1 = "app1" - app2 = "app2" - app3 = "app3" - app1Ns1 = "app1||ns1" - app2Ns2 = "app2||ns2" - app3Ns1 = "app3||ns1" - app1Ns4 = "app1||ns4" ) func TestLoadStandaloneConfiguration(t *testing.T) { @@ -305,604 +293,6 @@ func TestContainsKey(t *testing.T) { assert.True(t, containsKey(s, "b"), "unexpected result") } -func initializeAccessControlList(protocol string) (*AccessControlList, error) { - inputSpec := AccessControlSpec{ - DefaultAction: DenyAccess, - TrustDomain: "abcd", - AppPolicies: []AppPolicySpec{ - { - AppName: app1, - DefaultAction: AllowAccess, - TrustDomain: "public", - Namespace: "ns1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"POST", "GET"}, - Operation: "/op1", - }, - { - Action: DenyAccess, - HTTPVerb: []string{"*"}, - Operation: "/op2", - }, - }, - }, - { - AppName: app2, - DefaultAction: DenyAccess, - TrustDomain: "domain1", - Namespace: "ns2", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"PUT", "GET"}, - Operation: "/op3/a/*", - }, - { - Action: AllowAccess, - HTTPVerb: []string{"POST"}, - Operation: "/op4", - }, - }, - }, - { - AppName: app3, - TrustDomain: "domain1", - Namespace: "ns1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"POST"}, - Operation: "/op5", - }, - }, - }, - { - AppName: app1, // Duplicate app id with a different namespace - DefaultAction: AllowAccess, - TrustDomain: "public", - Namespace: "ns4", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"*"}, - Operation: "/op6", - }, - }, - }, - }, - } - accessControlList, err := ParseAccessControlSpec(inputSpec, protocol) - - return accessControlList, err -} - -func TestParseAccessControlSpec(t *testing.T) { - t.Run("translate to in-memory rules", func(t *testing.T) { - accessControlList, err := initializeAccessControlList(HTTPProtocol) - - assert.Nil(t, err) - - assert.Equal(t, DenyAccess, accessControlList.DefaultAction) - assert.Equal(t, "abcd", accessControlList.TrustDomain) - - // App1 - assert.Equal(t, app1, accessControlList.PolicySpec[app1Ns1].AppName) - assert.Equal(t, AllowAccess, accessControlList.PolicySpec[app1Ns1].DefaultAction) - assert.Equal(t, "public", accessControlList.PolicySpec[app1Ns1].TrustDomain) - assert.Equal(t, "ns1", accessControlList.PolicySpec[app1Ns1].Namespace) - - op1Actions := AccessControlListOperationAction{ - OperationPostFix: "/", - VerbAction: make(map[string]string), - } - op1Actions.VerbAction["POST"] = AllowAccess - op1Actions.VerbAction["GET"] = AllowAccess - op1Actions.OperationAction = AllowAccess - - op2Actions := AccessControlListOperationAction{ - OperationPostFix: "/", - VerbAction: make(map[string]string), - } - op2Actions.VerbAction["*"] = DenyAccess - op2Actions.OperationAction = DenyAccess - - assert.Equal(t, 2, len(accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op1"].VerbAction)) - assert.Equal(t, op1Actions, accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op1"]) - assert.Equal(t, 1, len(accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op2"].VerbAction)) - assert.Equal(t, op2Actions, accessControlList.PolicySpec[app1Ns1].AppOperationActions["/op2"]) - - // App2 - assert.Equal(t, app2, accessControlList.PolicySpec[app2Ns2].AppName) - assert.Equal(t, DenyAccess, accessControlList.PolicySpec[app2Ns2].DefaultAction) - assert.Equal(t, "domain1", accessControlList.PolicySpec[app2Ns2].TrustDomain) - assert.Equal(t, "ns2", accessControlList.PolicySpec[app2Ns2].Namespace) - - op3Actions := AccessControlListOperationAction{ - OperationPostFix: "/a/*", - VerbAction: make(map[string]string), - } - op3Actions.VerbAction["PUT"] = AllowAccess - op3Actions.VerbAction["GET"] = AllowAccess - op3Actions.OperationAction = AllowAccess - - op4Actions := AccessControlListOperationAction{ - OperationPostFix: "/", - VerbAction: make(map[string]string), - } - op4Actions.VerbAction["POST"] = AllowAccess - op4Actions.OperationAction = AllowAccess - - assert.Equal(t, 2, len(accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op3"].VerbAction)) - assert.Equal(t, op3Actions, accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op3"]) - assert.Equal(t, 1, len(accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op4"].VerbAction)) - assert.Equal(t, op4Actions, accessControlList.PolicySpec[app2Ns2].AppOperationActions["/op4"]) - - // App3 - assert.Equal(t, app3, accessControlList.PolicySpec[app3Ns1].AppName) - assert.Equal(t, "", accessControlList.PolicySpec[app3Ns1].DefaultAction) - assert.Equal(t, "domain1", accessControlList.PolicySpec[app3Ns1].TrustDomain) - assert.Equal(t, "ns1", accessControlList.PolicySpec[app3Ns1].Namespace) - - op5Actions := AccessControlListOperationAction{ - OperationPostFix: "/", - VerbAction: make(map[string]string), - } - op5Actions.VerbAction["POST"] = AllowAccess - op5Actions.OperationAction = AllowAccess - - assert.Equal(t, 1, len(accessControlList.PolicySpec[app3Ns1].AppOperationActions["/op5"].VerbAction)) - assert.Equal(t, op5Actions, accessControlList.PolicySpec[app3Ns1].AppOperationActions["/op5"]) - - // App1 with a different namespace - assert.Equal(t, app1, accessControlList.PolicySpec[app1Ns4].AppName) - assert.Equal(t, AllowAccess, accessControlList.PolicySpec[app1Ns4].DefaultAction) - assert.Equal(t, "public", accessControlList.PolicySpec[app1Ns4].TrustDomain) - assert.Equal(t, "ns4", accessControlList.PolicySpec[app1Ns4].Namespace) - - op6Actions := AccessControlListOperationAction{ - OperationPostFix: "/", - VerbAction: make(map[string]string), - } - op6Actions.VerbAction["*"] = AllowAccess - op6Actions.OperationAction = AllowAccess - - assert.Equal(t, 1, len(accessControlList.PolicySpec[app1Ns4].AppOperationActions["/op6"].VerbAction)) - assert.Equal(t, op6Actions, accessControlList.PolicySpec[app1Ns4].AppOperationActions["/op6"]) - }) - - t.Run("test when no trust domain and namespace specified in app policy", func(t *testing.T) { - invalidAccessControlSpec := AccessControlSpec{ - DefaultAction: DenyAccess, - TrustDomain: "public", - AppPolicies: []AppPolicySpec{ - { - AppName: app1, - DefaultAction: AllowAccess, - Namespace: "ns1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"POST", "GET"}, - Operation: "/op1", - }, - { - Action: DenyAccess, - HTTPVerb: []string{"*"}, - Operation: "/op2", - }, - }, - }, - { - AppName: app2, - DefaultAction: DenyAccess, - TrustDomain: "domain1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"PUT", "GET"}, - Operation: "/op3/a/*", - }, - { - Action: AllowAccess, - HTTPVerb: []string{"POST"}, - Operation: "/op4", - }, - }, - }, - { - AppName: "", - DefaultAction: DenyAccess, - TrustDomain: "domain1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"PUT", "GET"}, - Operation: "/op3/a/*", - }, - { - Action: AllowAccess, - HTTPVerb: []string{"POST"}, - Operation: "/op4", - }, - }, - }, - }, - } - - _, err := ParseAccessControlSpec(invalidAccessControlSpec, "http") - assert.Error(t, err, "invalid access control spec. missing trustdomain for apps: [%s], missing namespace for apps: [%s], missing app name on at least one of the app policies: true", app1, app2) - }) - - t.Run("test when no trust domain is specified for the app", func(t *testing.T) { - accessControlSpec := AccessControlSpec{ - DefaultAction: DenyAccess, - TrustDomain: "", - AppPolicies: []AppPolicySpec{ - { - AppName: app1, - DefaultAction: AllowAccess, - TrustDomain: "public", - Namespace: "ns1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"POST", "GET"}, - Operation: "/op1", - }, - { - Action: DenyAccess, - HTTPVerb: []string{"*"}, - Operation: "/op2", - }, - }, - }, - }, - } - - accessControlList, _ := ParseAccessControlSpec(accessControlSpec, "http") - assert.Equal(t, "public", accessControlList.PolicySpec[app1Ns1].TrustDomain) - }) - - t.Run("test when no access control policy has been specified", func(t *testing.T) { - invalidAccessControlSpec := AccessControlSpec{ - DefaultAction: "", - TrustDomain: "", - AppPolicies: []AppPolicySpec{}, - } - - accessControlList, _ := ParseAccessControlSpec(invalidAccessControlSpec, "http") - assert.Nil(t, accessControlList) - }) - - t.Run("test when no default global action has been specified", func(t *testing.T) { - invalidAccessControlSpec := AccessControlSpec{ - TrustDomain: "public", - AppPolicies: []AppPolicySpec{ - { - AppName: app1, - DefaultAction: AllowAccess, - TrustDomain: "domain1", - Namespace: "ns1", - AppOperationActions: []AppOperation{ - { - Action: AllowAccess, - HTTPVerb: []string{"POST", "GET"}, - Operation: "/op1", - }, - { - Action: DenyAccess, - HTTPVerb: []string{"*"}, - Operation: "/op2", - }, - }, - }, - }, - } - - accessControlList, _ := ParseAccessControlSpec(invalidAccessControlSpec, "http") - assert.Equal(t, accessControlList.DefaultAction, DenyAccess) - }) -} - -func TestSpiffeID(t *testing.T) { - t.Run("test parse spiffe id", func(t *testing.T) { - spiffeID := "spiffe://mydomain/ns/mynamespace/myappid" - id, err := parseSpiffeID(spiffeID) - assert.Equal(t, "mydomain", id.TrustDomain) - assert.Equal(t, "mynamespace", id.Namespace) - assert.Equal(t, "myappid", id.AppID) - assert.Nil(t, err) - }) - - t.Run("test parse invalid spiffe id", func(t *testing.T) { - spiffeID := "abcd" - _, err := parseSpiffeID(spiffeID) - assert.NotNil(t, err) - }) - - t.Run("test parse spiffe id with not all fields", func(t *testing.T) { - spiffeID := "spiffe://mydomain/ns/myappid" - _, err := parseSpiffeID(spiffeID) - assert.NotNil(t, err) - }) - - t.Run("test empty spiffe id", func(t *testing.T) { - spiffeID := "" - _, err := parseSpiffeID(spiffeID) - assert.NotNil(t, err) - }) -} - -func TestIsOperationAllowedByAccessControlPolicy(t *testing.T) { - t.Run("test when no acl specified", func(t *testing.T) { - srcAppID := app1 - spiffeID := SpiffeID{ - TrustDomain: "public", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, HTTPProtocol, nil) - // Action = Allow the operation since no ACL is defined - assert.True(t, isAllowed) - }) - - t.Run("test when no matching app in acl found", func(t *testing.T) { - srcAppID := "appX" - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "public", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Default global action - assert.False(t, isAllowed) - }) - - t.Run("test when trust domain does not match", func(t *testing.T) { - srcAppID := app1 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "private", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Ignore policy and apply global default action - assert.False(t, isAllowed) - }) - - t.Run("test when namespace does not match", func(t *testing.T) { - srcAppID := app1 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "public", - Namespace: "abcd", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op1", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Ignore policy and apply global default action - assert.False(t, isAllowed) - }) - - t.Run("test when spiffe id is nil", func(t *testing.T) { - srcAppID := app1 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(nil, srcAppID, "op1", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Default global action - assert.False(t, isAllowed) - }) - - t.Run("test when src app id is empty", func(t *testing.T) { - srcAppID := "" - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(nil, srcAppID, "op1", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Default global action - assert.False(t, isAllowed) - }) - - t.Run("test when operation is not found in the policy spec", func(t *testing.T) { - srcAppID := app1 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "public", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "opX", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Ignore policy and apply default action for app - assert.True(t, isAllowed) - }) - - t.Run("test http case-sensitivity when matching operation post fix", func(t *testing.T) { - srcAppID := app1 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "public", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "Op2", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Ignore policy and apply default action for app - assert.False(t, isAllowed) - }) - - t.Run("test when http verb is not found", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_PUT, HTTPProtocol, accessControlList) - // Action = Default action for the specific app - assert.False(t, isAllowed) - }) - - t.Run("test when default action for app is not specified and no matching http verb found", func(t *testing.T) { - srcAppID := app3 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op5", common.HTTPExtension_PUT, HTTPProtocol, accessControlList) - // Action = Global Default action - assert.False(t, isAllowed) - }) - - t.Run("test when http verb matches *", func(t *testing.T) { - srcAppID := app1 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "public", - Namespace: "ns1", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op2", common.HTTPExtension_PUT, HTTPProtocol, accessControlList) - // Action = Default action for the specific verb - assert.False(t, isAllowed) - }) - - t.Run("test when http verb matches a specific verb", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Default action for the specific verb - assert.True(t, isAllowed) - }) - - t.Run("test when operation is invoked with /", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op4", common.HTTPExtension_POST, HTTPProtocol, accessControlList) - // Action = Default action for the specific verb - assert.True(t, isAllowed) - }) - - t.Run("test when http verb is not specified", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_NONE, HTTPProtocol, accessControlList) - // Action = Default action for the app - assert.False(t, isAllowed) - }) - - t.Run("test when matching operation post fix is specified in policy spec", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op3/a", common.HTTPExtension_PUT, HTTPProtocol, accessControlList) - // Action = Default action for the specific verb - assert.True(t, isAllowed) - }) - - t.Run("test grpc case-sensitivity when matching operation post fix", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(GRPCProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/OP4", common.HTTPExtension_NONE, GRPCProtocol, accessControlList) - // Action = Default action for the specific verb - assert.False(t, isAllowed) - }) - - t.Run("test when non-matching operation post fix is specified in policy spec", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op3/b/b", common.HTTPExtension_PUT, HTTPProtocol, accessControlList) - // Action = Default action for the app - assert.False(t, isAllowed) - }) - - t.Run("test when non-matching operation post fix is specified in policy spec", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(HTTPProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "/op3/a/b", common.HTTPExtension_PUT, HTTPProtocol, accessControlList) - // Action = Default action for the app - assert.True(t, isAllowed) - }) - - t.Run("test with grpc invocation", func(t *testing.T) { - srcAppID := app2 - accessControlList, _ := initializeAccessControlList(GRPCProtocol) - spiffeID := SpiffeID{ - TrustDomain: "domain1", - Namespace: "ns2", - AppID: srcAppID, - } - isAllowed, _ := IsOperationAllowedByAccessControlPolicy(&spiffeID, srcAppID, "op4", common.HTTPExtension_NONE, GRPCProtocol, accessControlList) - // Action = Default action for the app - assert.True(t, isAllowed) - }) -} - -func TestGetOperationPrefixAndPostfix(t *testing.T) { - t.Run("test when operation single post fix exists", func(t *testing.T) { - operation := "/invoke/*" - prefix, postfix := getOperationPrefixAndPostfix(operation) - assert.Equal(t, "/invoke", prefix) - assert.Equal(t, "/*", postfix) - }) - - t.Run("test when operation longer post fix exists", func(t *testing.T) { - operation := "/invoke/a/*" - prefix, postfix := getOperationPrefixAndPostfix(operation) - assert.Equal(t, "/invoke", prefix) - assert.Equal(t, "/a/*", postfix) - }) - - t.Run("test when operation no post fix exists", func(t *testing.T) { - operation := "/invoke" - prefix, postfix := getOperationPrefixAndPostfix(operation) - assert.Equal(t, "/invoke", prefix) - assert.Equal(t, "/", postfix) - }) - - t.Run("test operation multi path post fix exists", func(t *testing.T) { - operation := "/invoke/a/b/*" - prefix, postfix := getOperationPrefixAndPostfix(operation) - assert.Equal(t, "/invoke", prefix) - assert.Equal(t, "/a/b/*", postfix) - }) -} - func TestFeatureEnabled(t *testing.T) { t.Run("Test feature enabled is correct", func(t *testing.T) { features := []FeatureSpec{ diff --git a/pkg/diagnostics/grpc_tracing.go b/pkg/diagnostics/grpc_tracing.go index bf4ae64bbc3..9530b3ca653 100644 --- a/pkg/diagnostics/grpc_tracing.go +++ b/pkg/diagnostics/grpc_tracing.go @@ -10,6 +10,8 @@ import ( "fmt" "strings" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/pkg/errors" "go.opencensus.io/trace" "go.opencensus.io/trace/propagation" "google.golang.org/grpc" @@ -23,7 +25,10 @@ import ( runtimev1pb "github.com/dapr/dapr/pkg/proto/runtime/v1" ) -const grpcTraceContextKey = "grpc-trace-bin" +const ( + grpcTraceContextKey = "grpc-trace-bin" + GRPCProxyAppIDKey = "dapr-app-id" +) // GRPCTraceUnaryServerInterceptor sets the trace context or starts the trace client span based on request. func GRPCTraceUnaryServerInterceptor(appID string, spec config.TracingSpec) grpc.UnaryServerInterceptor { @@ -47,26 +52,9 @@ func GRPCTraceUnaryServerInterceptor(appID string, spec config.TracingSpec) grpc } ctx, span = trace.StartSpanWithRemoteParent(ctx, spanName, sc, sampler, spanKind) - - var prefixedMetadata map[string]string - if span.SpanContext().TraceOptions.IsSampled() { - // users can add dapr- prefix if they want to see the header values in span attributes. - prefixedMetadata = userDefinedMetadata(ctx) - } - resp, err := handler(ctx, req) - if span.SpanContext().TraceOptions.IsSampled() { - // Populates dapr- prefixed header first - AddAttributesToSpan(span, prefixedMetadata) - spanAttr := spanAttributesMapFromGRPC(appID, req, info.FullMethod) - AddAttributesToSpan(span, spanAttr) - - // Correct the span name based on API. - if sname, ok := spanAttr[daprAPISpanNameInternal]; ok { - span.SetName(sname) - } - } + addSpanMetadataAndUpdateStatus(ctx, span, info.FullMethod, appID, req, false) // Add grpc-trace-bin header for all non-invocation api's if info.FullMethod != "/dapr.proto.runtime.v1.Dapr/InvokeService" { @@ -81,6 +69,72 @@ func GRPCTraceUnaryServerInterceptor(appID string, spec config.TracingSpec) grpc } } +// GRPCTraceStreamServerInterceptor sets the trace context or starts the trace client span based on request. +func GRPCTraceStreamServerInterceptor(appID string, spec config.TracingSpec) grpc.StreamServerInterceptor { + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + var span *trace.Span + spanName := info.FullMethod + + ctx := ss.Context() + md, _ := metadata.FromIncomingContext(ctx) + + vals := md.Get(GRPCProxyAppIDKey) + if len(vals) == 0 { + return errors.Errorf("cannot proxy request: missing %s metadata", GRPCProxyAppIDKey) + } + + targetID := vals[0] + wrapped := grpc_middleware.WrapServerStream(ss) + sc, _ := SpanContextFromIncomingGRPCMetadata(ctx) + sampler := diag_utils.TraceSampler(spec.SamplingRate) + + var spanKind trace.StartOption + + if appID == targetID { + spanKind = trace.WithSpanKind(trace.SpanKindServer) + } else { + spanKind = trace.WithSpanKind(trace.SpanKindClient) + } + + ctx, span = trace.StartSpanWithRemoteParent(ctx, spanName, sc, sampler, spanKind) + wrapped.WrappedContext = ctx + err := handler(srv, wrapped) + + addSpanMetadataAndUpdateStatus(ctx, span, info.FullMethod, appID, nil, true) + + UpdateSpanStatusFromGRPCError(span, err) + span.End() + + return err + } +} + +func addSpanMetadataAndUpdateStatus(ctx context.Context, span *trace.Span, fullMethod, appID string, req interface{}, stream bool) { + var prefixedMetadata map[string]string + if span.SpanContext().TraceOptions.IsSampled() { + // users can add dapr- prefix if they want to see the header values in span attributes. + prefixedMetadata = userDefinedMetadata(ctx) + } + + if span.SpanContext().TraceOptions.IsSampled() { + // Populates dapr- prefixed header first + AddAttributesToSpan(span, prefixedMetadata) + + spanAttr := map[string]string{} + if !stream { + spanAttr = spanAttributesMapFromGRPC(appID, req, fullMethod) + AddAttributesToSpan(span, spanAttr) + } else { + spanAttr[daprAPISpanNameInternal] = fullMethod + } + + // Correct the span name based on API. + if sname, ok := spanAttr[daprAPISpanNameInternal]; ok { + span.SetName(sname) + } + } +} + // userDefinedMetadata returns dapr- prefixed header from incoming metadata. // Users can add dapr- prefixed headers that they want to see in span attributes. func userDefinedMetadata(ctx context.Context) map[string]string { @@ -145,6 +199,7 @@ func SpanContextToGRPCMetadata(ctx context.Context, spanContext trace.SpanContex if (trace.SpanContext{}) == spanContext { return ctx } + traceContextBinary := propagation.Binary(spanContext) return metadata.AppendToOutgoingContext(ctx, grpcTraceContextKey, string(traceContextBinary)) } diff --git a/pkg/grpc/api.go b/pkg/grpc/api.go index cd06e542eb5..a15215cc420 100644 --- a/pkg/grpc/api.go +++ b/pkg/grpc/api.go @@ -12,7 +12,6 @@ import ( "strconv" "sync" - "github.com/PuerkitoBio/purell" "github.com/dapr/components-contrib/bindings" contrib_metadata "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/pubsub" @@ -26,6 +25,7 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" + "github.com/dapr/dapr/pkg/acl" "github.com/dapr/dapr/pkg/actors" components_v1alpha "github.com/dapr/dapr/pkg/apis/components/v1alpha1" "github.com/dapr/dapr/pkg/channel" @@ -164,7 +164,7 @@ func (a *api) CallLocal(ctx context.Context, in *internalv1pb.InternalInvokeRequ httpVerb = httpExt.GetVerb() } } - callAllowed, errMsg := a.applyAccessControlPolicies(ctx, operation, httpVerb, a.appProtocol) + callAllowed, errMsg := acl.ApplyAccessControlPolicies(ctx, operation, httpVerb, a.appProtocol, a.accessControlList) if !callAllowed { return nil, status.Errorf(codes.PermissionDenied, errMsg) @@ -180,48 +180,6 @@ func (a *api) CallLocal(ctx context.Context, in *internalv1pb.InternalInvokeRequ return resp.Proto(), err } -func normalizeOperation(operation string) (string, error) { - s, err := purell.NormalizeURLString(operation, purell.FlagsUsuallySafeGreedy|purell.FlagRemoveDuplicateSlashes) - if err != nil { - return "", err - } - return s, nil -} - -func (a *api) applyAccessControlPolicies(ctx context.Context, operation string, httpVerb commonv1pb.HTTPExtension_Verb, appProtocol string) (bool, string) { - // Apply access control list filter - spiffeID, err := config.GetAndParseSpiffeID(ctx) - if err != nil { - // Apply the default action - apiServerLogger.Debugf("error while reading spiffe id from client cert: %v. applying default global policy action", err.Error()) - } - var appID, trustDomain, namespace string - if spiffeID != nil { - appID = spiffeID.AppID - namespace = spiffeID.Namespace - trustDomain = spiffeID.TrustDomain - } - - operation, err = normalizeOperation(operation) - var errMessage string - - if err != nil { - errMessage = fmt.Sprintf("error in method normalization: %s", err) - apiServerLogger.Debugf(errMessage) - return false, errMessage - } - - action, actionPolicy := config.IsOperationAllowedByAccessControlPolicy(spiffeID, appID, operation, httpVerb, appProtocol, a.accessControlList) - emitACLMetrics(actionPolicy, appID, trustDomain, namespace, operation, httpVerb.String(), action) - - if !action { - errMessage = fmt.Sprintf("access control policy has denied access to appid: %s operation: %s verb: %s", appID, operation, httpVerb) - apiServerLogger.Debugf(errMessage) - } - - return action, errMessage -} - // CallActor invokes a virtual actor. func (a *api) CallActor(ctx context.Context, in *internalv1pb.InternalInvokeRequest) (*internalv1pb.InternalInvokeResponse, error) { req, err := invokev1.InternalInvokeRequest(in) @@ -1051,24 +1009,6 @@ func (a *api) isSecretAllowed(storeName, key string) bool { return true } -func emitACLMetrics(actionPolicy, appID, trustDomain, namespace, operation, verb string, action bool) { - if action { - switch actionPolicy { - case config.ActionPolicyApp: - diag.DefaultMonitoring.RequestAllowedByAppAction(appID, trustDomain, namespace, operation, verb, action) - case config.ActionPolicyGlobal: - diag.DefaultMonitoring.RequestAllowedByGlobalAction(appID, trustDomain, namespace, operation, verb, action) - } - } else { - switch actionPolicy { - case config.ActionPolicyApp: - diag.DefaultMonitoring.RequestBlockedByAppAction(appID, trustDomain, namespace, operation, verb, action) - case config.ActionPolicyGlobal: - diag.DefaultMonitoring.RequestBlockedByGlobalAction(appID, trustDomain, namespace, operation, verb, action) - } - } -} - func (a *api) SetAppChannel(appChannel channel.AppChannel) { a.appChannel = appChannel } diff --git a/pkg/grpc/api_test.go b/pkg/grpc/api_test.go index 8ae4df890ca..b5bcab8d5d8 100644 --- a/pkg/grpc/api_test.go +++ b/pkg/grpc/api_test.go @@ -1602,64 +1602,6 @@ func TestExtractEtag(t *testing.T) { }) } -func TestNormalizeOperation(t *testing.T) { - t.Run("normal path no slash", func(t *testing.T) { - p := "path" - p, _ = normalizeOperation(p) - - assert.Equal(t, "path", p) - }) - - t.Run("normal path caps", func(t *testing.T) { - p := "Path" - p, _ = normalizeOperation(p) - - assert.Equal(t, "Path", p) - }) - - t.Run("single slash", func(t *testing.T) { - p := "/path" - p, _ = normalizeOperation(p) - - assert.Equal(t, "/path", p) - }) - - t.Run("multiple slashes", func(t *testing.T) { - p := "///path" - p, _ = normalizeOperation(p) - - assert.Equal(t, "/path", p) - }) - - t.Run("prefix", func(t *testing.T) { - p := "../path" - p, _ = normalizeOperation(p) - - assert.Equal(t, "path", p) - }) - - t.Run("encoded", func(t *testing.T) { - p := "path%72" - p, _ = normalizeOperation(p) - - assert.Equal(t, "pathr", p) - }) - - t.Run("normal multiple paths", func(t *testing.T) { - p := "path1/path2/path3" - p, _ = normalizeOperation(p) - - assert.Equal(t, "path1/path2/path3", p) - }) - - t.Run("normal multiple paths leading slash", func(t *testing.T) { - p := "/path1/path2/path3" - p, _ = normalizeOperation(p) - - assert.Equal(t, "/path1/path2/path3", p) - }) -} - func GenerateStateOptionsTestCase() (*commonv1pb.StateOptions, state.SetStateOption) { concurrencyOption := commonv1pb.StateOptions_CONCURRENCY_FIRST_WRITE consistencyOption := commonv1pb.StateOptions_CONSISTENCY_STRONG diff --git a/pkg/grpc/grpc.go b/pkg/grpc/grpc.go index 6ccf68bfa79..e13d3b21e37 100644 --- a/pkg/grpc/grpc.go +++ b/pkg/grpc/grpc.go @@ -55,7 +55,7 @@ func (g *Manager) SetAuthenticator(auth security.Authenticator) { // CreateLocalChannel creates a new gRPC AppChannel. func (g *Manager) CreateLocalChannel(port, maxConcurrency int, spec config.TracingSpec, sslEnabled bool) (channel.AppChannel, error) { - conn, err := g.GetGRPCConnection(fmt.Sprintf("127.0.0.1:%v", port), "", "", true, false, sslEnabled) + conn, err := g.GetGRPCConnection(context.TODO(), fmt.Sprintf("127.0.0.1:%v", port), "", "", true, false, sslEnabled) if err != nil { return nil, errors.Errorf("error establishing connection to app grpc on port %v: %s", port, err) } @@ -66,7 +66,7 @@ func (g *Manager) CreateLocalChannel(port, maxConcurrency int, spec config.Traci } // GetGRPCConnection returns a new grpc connection for a given address and inits one if doesn't exist. -func (g *Manager) GetGRPCConnection(address, id string, namespace string, skipTLS, recreateIfExists, sslEnabled bool) (*grpc.ClientConn, error) { +func (g *Manager) GetGRPCConnection(ctx context.Context, address, id string, namespace string, skipTLS, recreateIfExists, sslEnabled bool, customOpts ...grpc.DialOption) (*grpc.ClientConn, error) { g.lock.RLock() if val, ok := g.connectionPool[address]; ok && !recreateIfExists { g.lock.RUnlock() @@ -112,7 +112,7 @@ func (g *Manager) GetGRPCConnection(address, id string, namespace string, skipTL transportCredentialsAdded = true } - ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) + ctx, cancel := context.WithTimeout(ctx, dialTimeout) defer cancel() dialPrefix := GetDialAddressPrefix(g.mode) @@ -128,6 +128,7 @@ func (g *Manager) GetGRPCConnection(address, id string, namespace string, skipTL opts = append(opts, grpc.WithInsecure()) } + opts = append(opts, customOpts...) conn, err := grpc.DialContext(ctx, dialPrefix+address, opts...) if err != nil { return nil, err diff --git a/pkg/grpc/grpc_test.go b/pkg/grpc/grpc_test.go index 51da75c04d4..197134774a8 100644 --- a/pkg/grpc/grpc_test.go +++ b/pkg/grpc/grpc_test.go @@ -6,6 +6,7 @@ package grpc import ( + "context" "crypto/x509" "fmt" "testing" @@ -50,9 +51,10 @@ func TestGetGRPCConnection(t *testing.T) { assert.NotNil(t, m) port := 55555 sslEnabled := false - conn, err := m.GetGRPCConnection(fmt.Sprintf("127.0.0.1:%v", port), "", "", true, true, sslEnabled) + ctx := context.TODO() + conn, err := m.GetGRPCConnection(ctx, fmt.Sprintf("127.0.0.1:%v", port), "", "", true, true, sslEnabled) assert.NoError(t, err) - conn2, err2 := m.GetGRPCConnection(fmt.Sprintf("127.0.0.1:%v", port), "", "", true, true, sslEnabled) + conn2, err2 := m.GetGRPCConnection(ctx, fmt.Sprintf("127.0.0.1:%v", port), "", "", true, true, sslEnabled) assert.NoError(t, err2) assert.Equal(t, connectivity.Shutdown, conn.GetState()) conn2.Close() @@ -63,7 +65,8 @@ func TestGetGRPCConnection(t *testing.T) { assert.NotNil(t, m) port := 55555 sslEnabled := true - _, err := m.GetGRPCConnection(fmt.Sprintf("127.0.0.1:%v", port), "", "", true, true, sslEnabled) + ctx := context.TODO() + _, err := m.GetGRPCConnection(ctx, fmt.Sprintf("127.0.0.1:%v", port), "", "", true, true, sslEnabled) assert.NoError(t, err) }) } diff --git a/pkg/grpc/proxy/codec/codec.go b/pkg/grpc/proxy/codec/codec.go new file mode 100644 index 00000000000..7c2976d7571 --- /dev/null +++ b/pkg/grpc/proxy/codec/codec.go @@ -0,0 +1,95 @@ +// Copyright Michal Witkowski. +// Code is based on https://github.com/trusch/grpc-proxy + +package codec + +import ( + "github.com/golang/protobuf/proto" + "google.golang.org/grpc/encoding" +) + +// Name is the name by which the proxy codec is registered in the encoding codec registry +// We have to say that we are the "proto" codec otherwise marshaling will fail. +const Name = "proto" + +func init() { + Register() +} + +// Register manually registers the codec. +func Register() { + encoding.RegisterCodec(codec()) +} + +// codec returns a proxying grpc.codec with the default protobuf codec as parent. +// +// See CodecWithParent. +func codec() encoding.Codec { + // since we have registered the default codec by importing it, + // we can fetch it from the registry and use it as our parent + // and overwrite the existing codec in the registry + return codecWithParent(&protoCodec{}) +} + +// CodecWithParent returns a proxying grpc.Codec with a user provided codec as parent. +// +// This codec is *crucial* to the functioning of the proxy. It allows the proxy server to be oblivious +// to the schema of the forwarded messages. It basically treats a gRPC message frame as raw bytes. +// However, if the server handler, or the client caller are not proxy-internal functions it will fall back +// to trying to decode the message using a fallback codec. +func codecWithParent(fallback encoding.Codec) encoding.Codec { + return &Proxy{parentCodec: fallback} +} + +// Proxy satisfies the encoding.Codec interface. +type Proxy struct { + parentCodec encoding.Codec +} + +// Frame holds the proxy transported data. +type Frame struct { + payload []byte +} + +// ProtoMessage tags a frame as valid proto message. +func (f *Frame) ProtoMessage() {} + +// Marshal implents the encoding.Codec interface method. +func (p *Proxy) Marshal(v interface{}) ([]byte, error) { + out, ok := v.(*Frame) + if !ok { + return p.parentCodec.Marshal(v) + } + + return out.payload, nil +} + +// Unmarshal implents the encoding.Codec interface method. +func (p *Proxy) Unmarshal(data []byte, v interface{}) error { + dst, ok := v.(*Frame) + if !ok { + return p.parentCodec.Unmarshal(data, v) + } + dst.payload = data + return nil +} + +// Name implents the encoding.Codec interface method. +func (*Proxy) Name() string { + return Name +} + +// protoCodec is a Codec implementation with protobuf. It is the default rawCodec for gRPC. +type protoCodec struct{} + +func (*protoCodec) Marshal(v interface{}) ([]byte, error) { + return proto.Marshal(v.(proto.Message)) +} + +func (*protoCodec) Unmarshal(data []byte, v interface{}) error { + return proto.Unmarshal(data, v.(proto.Message)) +} + +func (*protoCodec) Name() string { + return "proxy>proto" +} diff --git a/pkg/grpc/proxy/codec/codec_test.go b/pkg/grpc/proxy/codec/codec_test.go new file mode 100644 index 00000000000..ddbeae0b047 --- /dev/null +++ b/pkg/grpc/proxy/codec/codec_test.go @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation, Dapr Contributors and Michal Witkowski. +// Code is based on https://github.com/trusch/grpc-proxy + +package codec + +import ( + "testing" + + _ "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/require" + pb "github.com/trusch/grpc-proxy/testservice" + "google.golang.org/grpc/encoding" +) + +func TestCodec_ReadYourWrites(t *testing.T) { + framePtr := &Frame{} + data := []byte{0xDE, 0xAD, 0xBE, 0xEF} + Register() + codec := encoding.GetCodec((&Proxy{}).Name()) + require.NotNil(t, codec, "codec must be registered") + require.NoError(t, codec.Unmarshal(data, framePtr), "unmarshalling must go ok") + out, err := codec.Marshal(framePtr) + require.NoError(t, err, "no marshal error") + require.Equal(t, data, out, "output and data must be the same") + + // reuse + require.NoError(t, codec.Unmarshal([]byte{0x55}, framePtr), "unmarshalling must go ok") + out, err = codec.Marshal(framePtr) + require.NoError(t, err, "no marshal error") + require.Equal(t, []byte{0x55}, out, "output and data must be the same") +} + +func TestProtoCodec_ReadYourWrites(t *testing.T) { + p1 := &pb.PingRequest{ + Value: "test-ping", + } + proxyCd := encoding.GetCodec((&Proxy{}).Name()) + + require.NotNil(t, proxyCd, "proxy codec must not be nil") + + out1p1, err := proxyCd.Marshal(p1) + require.NoError(t, err, "marshalling must go ok") + out2p1, err := proxyCd.Marshal(p1) + require.NoError(t, err, "marshalling must go ok") + + p2 := &pb.PingRequest{} + err = proxyCd.Unmarshal(out1p1, p2) + require.NoError(t, err, "unmarshalling must go ok") + err = proxyCd.Unmarshal(out2p1, p2) + require.NoError(t, err, "unmarshalling must go ok") + + require.Equal(t, *p1, *p2) +} diff --git a/pkg/grpc/proxy/director.go b/pkg/grpc/proxy/director.go new file mode 100644 index 00000000000..079ff9a6282 --- /dev/null +++ b/pkg/grpc/proxy/director.go @@ -0,0 +1,24 @@ +// Copyright Michal Witkowski. +// Code is based on https://github.com/trusch/grpc-proxy + +package proxy + +import ( + "golang.org/x/net/context" + "google.golang.org/grpc" +) + +// StreamDirector returns a gRPC ClientConn to be used to forward the call to. +// +// The presence of the `Context` allows for rich filtering, e.g. based on Metadata (headers). +// If no handling is meant to be done, a `codes.NotImplemented` gRPC error should be returned. +// +// The context returned from this function should be the context for the *outgoing* (to backend) call. In case you want +// to forward any Metadata between the inbound request and outbound requests, you should do it manually. However, you +// *must* propagate the cancel function (`context.WithCancel`) of the inbound context to the one returned. +// +// It is worth noting that the StreamDirector will be fired *after* all server-side stream interceptors +// are invoked. So decisions around authorization, monitoring etc. are better to be handled there. +// +// See the rather rich example. +type StreamDirector func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) diff --git a/pkg/grpc/proxy/handler.go b/pkg/grpc/proxy/handler.go new file mode 100644 index 00000000000..a339f11b55c --- /dev/null +++ b/pkg/grpc/proxy/handler.go @@ -0,0 +1,165 @@ +// Copyright Michal Witkowski. +// Code is based on https://github.com/trusch/grpc-proxy + +package proxy + +import ( + "io" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/dapr/dapr/pkg/grpc/proxy/codec" +) + +var ( + clientStreamDescForProxying = &grpc.StreamDesc{ + ServerStreams: true, + ClientStreams: true, + } +) + +// RegisterService sets up a proxy handler for a particular gRPC service and method. +// The behaviour is the same as if you were registering a handler method, e.g. from a codegenerated pb.go file. +// +// This can *only* be used if the `server` also uses grpcproxy.CodecForServer() ServerOption. +func RegisterService(server *grpc.Server, director StreamDirector, serviceName string, methodNames ...string) { + streamer := &handler{director} + fakeDesc := &grpc.ServiceDesc{ + ServiceName: serviceName, + HandlerType: (*interface{})(nil), + } + for _, m := range methodNames { + streamDesc := grpc.StreamDesc{ + StreamName: m, + Handler: streamer.handler, + ServerStreams: true, + ClientStreams: true, + } + fakeDesc.Streams = append(fakeDesc.Streams, streamDesc) + } + server.RegisterService(fakeDesc, streamer) +} + +// TransparentHandler returns a handler that attempts to proxy all requests that are not registered in the server. +// The indented use here is as a transparent proxy, where the server doesn't know about the services implemented by the +// backends. It should be used as a `grpc.UnknownServiceHandler`. +// +// This can *only* be used if the `server` also uses grpcproxy.CodecForServer() ServerOption. +func TransparentHandler(director StreamDirector) grpc.StreamHandler { + streamer := &handler{director} + return streamer.handler +} + +type handler struct { + director StreamDirector +} + +// handler is where the real magic of proxying happens. +// It is invoked like any gRPC server stream and uses the gRPC server framing to get and receive bytes from the wire, +// forwarding it to a ClientStream established against the relevant ClientConn. +func (s *handler) handler(srv interface{}, serverStream grpc.ServerStream) error { + // little bit of gRPC internals never hurt anyone + fullMethodName, ok := grpc.MethodFromServerStream(serverStream) + if !ok { + return status.Errorf(codes.Internal, "lowLevelServerStream not exists in context") + } + // We require that the director's returned context inherits from the serverStream.Context(). + outgoingCtx, backendConn, err := s.director(serverStream.Context(), fullMethodName) + if err != nil { + return err + } + + clientCtx, clientCancel := context.WithCancel(outgoingCtx) + // TODO(mwitkow): Add a `forwarded` header to metadata, https://en.wikipedia.org/wiki/X-Forwarded-For. + clientStream, err := grpc.NewClientStream(clientCtx, clientStreamDescForProxying, backendConn, fullMethodName, grpc.CallContentSubtype((&codec.Proxy{}).Name())) + if err != nil { + return err + } + // Explicitly *do not close* s2cErrChan and c2sErrChan, otherwise the select below will not terminate. + // Channels do not have to be closed, it is just a control flow mechanism, see + // https://groups.google.com/forum/#!msg/golang-nuts/pZwdYRGxCIk/qpbHxRRPJdUJ + s2cErrChan := s.forwardServerToClient(serverStream, clientStream) + c2sErrChan := s.forwardClientToServer(clientStream, serverStream) + // We don't know which side is going to stop sending first, so we need a select between the two. + for i := 0; i < 2; i++ { + select { + case s2cErr := <-s2cErrChan: + if s2cErr == io.EOF { + // this is the happy case where the sender has encountered io.EOF, and won't be sending anymore./ + // the clientStream>serverStream may continue pumping though. + clientStream.CloseSend() + continue + } else { + // however, we may have gotten a receive error (stream disconnected, a read error etc) in which case we need + // to cancel the clientStream to the backend, let all of its goroutines be freed up by the CancelFunc and + // exit with an error to the stack + clientCancel() + return status.Errorf(codes.Internal, "failed proxying s2c: %v", s2cErr) + } + case c2sErr := <-c2sErrChan: + // This happens when the clientStream has nothing else to offer (io.EOF), returned a gRPC error. In those two + // cases we may have received Trailers as part of the call. In case of other errors (stream closed) the trailers + // will be nil. + serverStream.SetTrailer(clientStream.Trailer()) + // c2sErr will contain RPC error from client code. If not io.EOF return the RPC error as server stream error. + if c2sErr != io.EOF { + return c2sErr + } + return nil + } + } + return status.Errorf(codes.Internal, "gRPC proxying should never reach this stage.") +} + +func (s *handler) forwardClientToServer(src grpc.ClientStream, dst grpc.ServerStream) chan error { + ret := make(chan error, 1) + go func() { + f := &codec.Frame{} + for i := 0; ; i++ { + if err := src.RecvMsg(f); err != nil { + ret <- err // this can be io.EOF which is happy case + break + } + if i == 0 { + // This is a bit of a hack, but client to server headers are only readable after first client msg is + // received but must be written to server stream before the first msg is flushed. + // This is the only place to do it nicely. + md, err := src.Header() + if err != nil { + ret <- err + break + } + if err := dst.SendHeader(md); err != nil { + ret <- err + break + } + } + if err := dst.SendMsg(f); err != nil { + ret <- err + break + } + } + }() + return ret +} + +func (s *handler) forwardServerToClient(src grpc.ServerStream, dst grpc.ClientStream) chan error { + ret := make(chan error, 1) + go func() { + f := &codec.Frame{} + for i := 0; ; i++ { + if err := src.RecvMsg(f); err != nil { + ret <- err // this can be io.EOF which is happy case + break + } + if err := dst.SendMsg(f); err != nil { + ret <- err + break + } + } + }() + return ret +} diff --git a/pkg/grpc/proxy/handler_test.go b/pkg/grpc/proxy/handler_test.go new file mode 100644 index 00000000000..f889c5e58ba --- /dev/null +++ b/pkg/grpc/proxy/handler_test.go @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft Corporation, Dapr Contributors and Michal Witkowski. +// Code is based on https://github.com/trusch/grpc-proxy + +package proxy + +import ( + "fmt" + "io" + "net" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + pb "github.com/trusch/grpc-proxy/testservice" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + codec "github.com/dapr/dapr/pkg/grpc/proxy/codec" +) + +const ( + pingDefaultValue = "I like kittens." + clientMdKey = "test-client-header" + serverHeaderMdKey = "test-client-header" + serverTrailerMdKey = "test-client-trailer" + + rejectingMdKey = "test-reject-rpc-if-in-context" + + countListResponses = 20 +) + +// asserting service is implemented on the server side and serves as a handler for stuff. +type assertingService struct { + t *testing.T +} + +func (s *assertingService) PingEmpty(ctx context.Context, _ *pb.Empty) (*pb.PingResponse, error) { + // Check that this call has client's metadata. + md, ok := metadata.FromIncomingContext(ctx) + assert.True(s.t, ok, "PingEmpty call must have metadata in context") + _, ok = md[clientMdKey] + assert.True(s.t, ok, "PingEmpty call must have clients's custom headers in metadata") + return &pb.PingResponse{Value: pingDefaultValue, Counter: 42}, nil +} + +func (s *assertingService) Ping(ctx context.Context, ping *pb.PingRequest) (*pb.PingResponse, error) { + // Send user trailers and headers. + grpc.SendHeader(ctx, metadata.Pairs(serverHeaderMdKey, "I like turtles.")) + grpc.SetTrailer(ctx, metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) + return &pb.PingResponse{Value: ping.Value, Counter: 42}, nil +} + +func (s *assertingService) PingError(ctx context.Context, ping *pb.PingRequest) (*pb.Empty, error) { + return nil, status.Errorf(codes.FailedPrecondition, "Userspace error.") +} + +func (s *assertingService) PingList(ping *pb.PingRequest, stream pb.TestService_PingListServer) error { + // Send user trailers and headers. + stream.SendHeader(metadata.Pairs(serverHeaderMdKey, "I like turtles.")) + for i := 0; i < countListResponses; i++ { + stream.Send(&pb.PingResponse{Value: ping.Value, Counter: int32(i)}) + } + stream.SetTrailer(metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) + return nil +} + +func (s *assertingService) PingStream(stream pb.TestService_PingStreamServer) error { + stream.SendHeader(metadata.Pairs(serverHeaderMdKey, "I like turtles.")) + counter := int32(0) + for { + ping, err := stream.Recv() + if err == io.EOF { + break + } else if err != nil { + require.NoError(s.t, err, "can't fail reading stream") + return err + } + pong := &pb.PingResponse{Value: ping.Value, Counter: counter} + if err := stream.Send(pong); err != nil { + require.NoError(s.t, err, "can't fail sending back a pong") + } + counter++ + } + stream.SetTrailer(metadata.Pairs(serverTrailerMdKey, "I like ending turtles.")) + return nil +} + +// ProxyHappySuite tests the "happy" path of handling: that everything works in absence of connection issues. +type ProxyHappySuite struct { + suite.Suite + + serverListener net.Listener + server *grpc.Server + proxyListener net.Listener + proxy *grpc.Server + serverClientConn *grpc.ClientConn + + client *grpc.ClientConn + testClient pb.TestServiceClient +} + +func (s *ProxyHappySuite) ctx() context.Context { + // Make all RPC calls last at most 1 sec, meaning all async issues or deadlock will not kill tests. + ctx, _ := context.WithTimeout(context.TODO(), 120*time.Second) + return ctx +} + +func (s *ProxyHappySuite) TestPingEmptyCarriesClientMetadata() { + // s.T().Skip() + ctx := metadata.NewOutgoingContext(s.ctx(), metadata.Pairs(clientMdKey, "true")) + out, err := s.testClient.PingEmpty(ctx, &pb.Empty{}) + require.NoError(s.T(), err, "PingEmpty should succeed without errors") + require.Equal(s.T(), &pb.PingResponse{Value: pingDefaultValue, Counter: 42}, out) +} + +func (s *ProxyHappySuite) TestPingEmpty_StressTest() { + for i := 0; i < 50; i++ { + s.TestPingEmptyCarriesClientMetadata() + } +} + +func (s *ProxyHappySuite) TestPingCarriesServerHeadersAndTrailers() { + // s.T().Skip() + headerMd := make(metadata.MD) + trailerMd := make(metadata.MD) + // This is an awkward calling convention... but meh. + out, err := s.testClient.Ping(s.ctx(), &pb.PingRequest{Value: "foo"}, grpc.Header(&headerMd), grpc.Trailer(&trailerMd)) + require.NoError(s.T(), err, "Ping should succeed without errors") + require.Equal(s.T(), &pb.PingResponse{Value: "foo", Counter: 42}, out) + assert.Contains(s.T(), headerMd, serverHeaderMdKey, "server response headers must contain server data") + assert.Len(s.T(), trailerMd, 1, "server response trailers must contain server data") +} + +func (s *ProxyHappySuite) TestPingErrorPropagatesAppError() { + _, err := s.testClient.PingError(s.ctx(), &pb.PingRequest{Value: "foo"}) + require.Error(s.T(), err, "PingError should never succeed") + st, ok := status.FromError(err) + require.True(s.T(), ok, "must get status from error") + assert.Equal(s.T(), codes.FailedPrecondition, st.Code()) + assert.Equal(s.T(), "Userspace error.", st.Message()) +} + +func (s *ProxyHappySuite) TestDirectorErrorIsPropagated() { + // See SetupSuite where the StreamDirector has a special case. + ctx := metadata.NewOutgoingContext(s.ctx(), metadata.Pairs(rejectingMdKey, "true")) + _, err := s.testClient.Ping(ctx, &pb.PingRequest{Value: "foo"}) + require.Error(s.T(), err, "Director should reject this RPC") + st, ok := status.FromError(err) + require.True(s.T(), ok, "must get status from error") + assert.Equal(s.T(), codes.PermissionDenied, st.Code()) + assert.Equal(s.T(), "testing rejection", st.Message()) +} + +func (s *ProxyHappySuite) TestPingStream_FullDuplexWorks() { + stream, err := s.testClient.PingStream(s.ctx()) + require.NoError(s.T(), err, "PingStream request should be successful.") + + for i := 0; i < countListResponses; i++ { + ping := &pb.PingRequest{Value: fmt.Sprintf("foo:%d", i)} + require.NoError(s.T(), stream.Send(ping), "sending to PingStream must not fail") + resp, sErr := stream.Recv() + if sErr == io.EOF { + break + } + if i == 0 { + // Check that the header arrives before all entries. + headerMd, hErr := stream.Header() + require.NoError(s.T(), hErr, "PingStream headers should not error.") + assert.Contains(s.T(), headerMd, serverHeaderMdKey, "PingStream response headers user contain metadata") + } + require.NotNil(s.T(), resp, "resp must not be nil") + assert.EqualValues(s.T(), i, resp.Counter, "ping roundtrip must succeed with the correct id") + } + require.NoError(s.T(), stream.CloseSend(), "no error on close send") + _, err = stream.Recv() + require.Equal(s.T(), io.EOF, err, "stream should close with io.EOF, meaining OK") + // Check that the trailer headers are here. + trailerMd := stream.Trailer() + assert.Len(s.T(), trailerMd, 1, "PingList trailer headers user contain metadata") +} + +func (s *ProxyHappySuite) TestPingStream_StressTest() { + for i := 0; i < 50; i++ { + s.TestPingStream_FullDuplexWorks() + } +} + +func (s *ProxyHappySuite) SetupSuite() { + var err error + + pc := encoding.GetCodec((&codec.Proxy{}).Name()) + dc := encoding.GetCodec("proto") + require.NotNil(s.T(), pc, "proxy codec must be registered") + require.NotNil(s.T(), dc, "default codec must be registered") + + s.proxyListener, err = net.Listen("tcp", "127.0.0.1:0") + require.NoError(s.T(), err, "must be able to allocate a port for proxyListener") + s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") + require.NoError(s.T(), err, "must be able to allocate a port for serverListener") + + grpclog.SetLoggerV2(testingLog{s.T()}) + + s.server = grpc.NewServer() + pb.RegisterTestServiceServer(s.server, &assertingService{t: s.T()}) + + // Setup of the proxy's Director. + s.serverClientConn, err = grpc.Dial( + s.serverListener.Addr().String(), + grpc.WithInsecure(), + grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name())), + ) + require.NoError(s.T(), err, "must not error on deferred client Dial") + director := func(ctx context.Context, fullName string) (context.Context, *grpc.ClientConn, error) { + md, ok := metadata.FromIncomingContext(ctx) + if ok { + if _, exists := md[rejectingMdKey]; exists { + return ctx, nil, status.Errorf(codes.PermissionDenied, "testing rejection") + } + } + // Explicitly copy the metadata, otherwise the tests will fail. + outCtx, _ := context.WithCancel(ctx) + outCtx = metadata.NewOutgoingContext(outCtx, md.Copy()) + return outCtx, s.serverClientConn, nil + } + s.proxy = grpc.NewServer( + grpc.UnknownServiceHandler(TransparentHandler(director)), + ) + // Ping handler is handled as an explicit registration and not as a TransparentHandler. + RegisterService(s.proxy, director, + "mwitkow.testproto.TestService", + "Ping") + + // Start the serving loops. + s.T().Logf("starting grpc.Server at: %v", s.serverListener.Addr().String()) + go func() { + s.server.Serve(s.serverListener) + }() + s.T().Logf("starting grpc.Proxy at: %v", s.proxyListener.Addr().String()) + go func() { + s.proxy.Serve(s.proxyListener) + }() + + time.Sleep(time.Second) + + ctx, cancel := context.WithTimeout(context.TODO(), time.Second*1) + defer cancel() + + clientConn, err := grpc.DialContext( + ctx, + strings.Replace(s.proxyListener.Addr().String(), "127.0.0.1", "localhost", 1), + grpc.WithInsecure(), + grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name())), + ) + require.NoError(s.T(), err, "must not error on deferred client Dial") + s.testClient = pb.NewTestServiceClient(clientConn) +} + +func (s *ProxyHappySuite) TearDownSuite() { + if s.client != nil { + s.client.Close() + } + if s.serverClientConn != nil { + s.serverClientConn.Close() + } + // Close all transports so the logs don't get spammy. + time.Sleep(10 * time.Millisecond) + if s.proxy != nil { + s.proxy.Stop() + s.proxyListener.Close() + } + if s.serverListener != nil { + s.server.Stop() + s.serverListener.Close() + } +} + +func TestProxyHappySuite(t *testing.T) { + suite.Run(t, &ProxyHappySuite{}) +} + +// Abstraction that allows us to pass the *testing.T as a grpclogger. +type testingLog struct { + T *testing.T +} + +// Info logs to INFO log. Arguments are handled in the manner of fmt.Print. +func (t testingLog) Info(args ...interface{}) { +} + +// Infoln logs to INFO log. Arguments are handled in the manner of fmt.Println. +func (t testingLog) Infoln(args ...interface{}) { +} + +// Infof logs to INFO log. Arguments are handled in the manner of fmt.Printf. +func (t testingLog) Infof(format string, args ...interface{}) { +} + +// Warning logs to WARNING log. Arguments are handled in the manner of fmt.Print. +func (t testingLog) Warning(args ...interface{}) { +} + +// Warningln logs to WARNING log. Arguments are handled in the manner of fmt.Println. +func (t testingLog) Warningln(args ...interface{}) { +} + +// Warningf logs to WARNING log. Arguments are handled in the manner of fmt.Printf. +func (t testingLog) Warningf(format string, args ...interface{}) { +} + +// Error logs to ERROR log. Arguments are handled in the manner of fmt.Print. +func (t testingLog) Error(args ...interface{}) { + t.T.Error(args...) +} + +// Errorln logs to ERROR log. Arguments are handled in the manner of fmt.Println. +func (t testingLog) Errorln(args ...interface{}) { + t.T.Error(args...) +} + +// Errorf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. +func (t testingLog) Errorf(format string, args ...interface{}) { + t.T.Errorf(format, args...) +} + +// Fatal logs to ERROR log. Arguments are handled in the manner of fmt.Print. +// gRPC ensures that all Fatal logs will exit with os.Exit(1). +// Implementations may also call os.Exit() with a non-zero exit code. +func (t testingLog) Fatal(args ...interface{}) { + t.T.Fatal(args...) +} + +// Fatalln logs to ERROR log. Arguments are handled in the manner of fmt.Println. +// gRPC ensures that all Fatal logs will exit with os.Exit(1). +// Implementations may also call os.Exit() with a non-zero exit code. +func (t testingLog) Fatalln(args ...interface{}) { + t.T.Fatal(args...) +} + +// Fatalf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. +// gRPC ensures that all Fatal logs will exit with os.Exit(1). +// Implementations may also call os.Exit() with a non-zero exit code. +func (t testingLog) Fatalf(format string, args ...interface{}) { + t.T.Fatalf(format, args...) +} + +// V reports whether verbosity level l is at least the requested verbose level. +func (t testingLog) V(l int) bool { + return true +} diff --git a/pkg/grpc/server.go b/pkg/grpc/server.go index 5162cbcef85..5eac8f083bf 100644 --- a/pkg/grpc/server.go +++ b/pkg/grpc/server.go @@ -23,6 +23,7 @@ import ( "github.com/dapr/dapr/pkg/config" diag "github.com/dapr/dapr/pkg/diagnostics" diag_utils "github.com/dapr/dapr/pkg/diagnostics/utils" + "github.com/dapr/dapr/pkg/messaging" internalv1pb "github.com/dapr/dapr/pkg/proto/internals/v1" runtimev1pb "github.com/dapr/dapr/pkg/proto/runtime/v1" auth "github.com/dapr/dapr/pkg/runtime/security" @@ -58,13 +59,14 @@ type server struct { maxConnectionAge *time.Duration authToken string apiSpec config.APISpec + proxy messaging.Proxy } var apiServerLogger = logger.NewLogger("dapr.runtime.grpc.api") var internalServerLogger = logger.NewLogger("dapr.runtime.grpc.internal") // NewAPIServer returns a new user facing gRPC API server. -func NewAPIServer(api API, config ServerConfig, tracingSpec config.TracingSpec, metricSpec config.MetricSpec, apiSpec config.APISpec) Server { +func NewAPIServer(api API, config ServerConfig, tracingSpec config.TracingSpec, metricSpec config.MetricSpec, apiSpec config.APISpec, proxy messaging.Proxy) Server { return &server{ api: api, config: config, @@ -74,11 +76,12 @@ func NewAPIServer(api API, config ServerConfig, tracingSpec config.TracingSpec, logger: apiServerLogger, authToken: auth.GetAPIToken(), apiSpec: apiSpec, + proxy: proxy, } } // NewInternalServer returns a new gRPC server for Dapr to Dapr communications. -func NewInternalServer(api API, config ServerConfig, tracingSpec config.TracingSpec, metricSpec config.MetricSpec, authenticator auth.Authenticator) Server { +func NewInternalServer(api API, config ServerConfig, tracingSpec config.TracingSpec, metricSpec config.MetricSpec, authenticator auth.Authenticator, proxy messaging.Proxy) Server { return &server{ api: api, config: config, @@ -89,6 +92,7 @@ func NewInternalServer(api API, config ServerConfig, tracingSpec config.TracingS kind: internalServer, logger: internalServerLogger, maxConnectionAge: getDefaultMaxAgeDuration(), + proxy: proxy, } } @@ -116,6 +120,7 @@ func (s *server) StartNonBlocking() error { } else if s.kind == apiServer { runtimev1pb.RegisterDaprServer(server, s.api) } + go func() { if err := server.Serve(lis); err != nil { s.logger.Fatalf("gRPC serve error: %v", err) @@ -146,6 +151,7 @@ func (s *server) generateWorkloadCert() error { func (s *server) getMiddlewareOptions() []grpc_go.ServerOption { opts := []grpc_go.ServerOption{} intr := []grpc_go.UnaryServerInterceptor{} + intrStream := []grpc_go.StreamServerInterceptor{} if len(s.apiSpec.Allowed) > 0 { s.logger.Info("enabled API access list on gRPC server") @@ -160,6 +166,10 @@ func (s *server) getMiddlewareOptions() []grpc_go.ServerOption { if diag_utils.IsTracingEnabled(s.tracingSpec.SamplingRate) { s.logger.Info("enabled gRPC tracing middleware") intr = append(intr, diag.GRPCTraceUnaryServerInterceptor(s.config.AppID, s.tracingSpec)) + + if s.proxy != nil { + intrStream = append(intrStream, diag.GRPCTraceStreamServerInterceptor(s.config.AppID, s.tracingSpec)) + } } if s.metricSpec.Enabled { @@ -174,6 +184,15 @@ func (s *server) getMiddlewareOptions() []grpc_go.ServerOption { opts, grpc_go.UnaryInterceptor(chain), ) + + if s.proxy != nil { + chainStream := grpc_middleware.ChainStreamServer( + intrStream..., + ) + + opts = append(opts, grpc_go.StreamInterceptor(chainStream)) + } + return opts } @@ -205,6 +224,10 @@ func (s *server) getGRPCServer() (*grpc_go.Server, error) { opts = append(opts, grpc_go.MaxRecvMsgSize(s.config.MaxRequestBodySize*1024*1024), grpc_go.MaxSendMsgSize(s.config.MaxRequestBodySize*1024*1024)) + if s.proxy != nil { + opts = append(opts, grpc_go.UnknownServiceHandler(s.proxy.Handler())) + } + return grpc_go.NewServer(opts...), nil } diff --git a/pkg/messaging/direct_messaging.go b/pkg/messaging/direct_messaging.go index 483b63c67ce..da3804a2345 100644 --- a/pkg/messaging/direct_messaging.go +++ b/pkg/messaging/direct_messaging.go @@ -36,7 +36,7 @@ var log = logger.NewLogger("dapr.runtime.direct_messaging") // messageClientConnection is the function type to connect to the other // applications to send the message using service invocation. -type messageClientConnection func(address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool) (*grpc.ClientConn, error) +type messageClientConnection func(ctx context.Context, address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool, customOpts ...grpc.DialOption) (*grpc.ClientConn, error) // DirectMessaging is the API interface for invoking a remote app. type DirectMessaging interface { @@ -55,6 +55,7 @@ type directMessaging struct { hostAddress string hostName string maxRequestBodySize int + proxy Proxy } type remoteApp struct { @@ -70,10 +71,11 @@ func NewDirectMessaging( appChannel channel.AppChannel, clientConnFn messageClientConnection, resolver nr.Resolver, - tracingSpec config.TracingSpec, maxRequestBodySize int) DirectMessaging { + tracingSpec config.TracingSpec, maxRequestBodySize int, proxy Proxy) DirectMessaging { hAddr, _ := utils.GetHostAddress() hName, _ := os.Hostname() - return &directMessaging{ + + dm := &directMessaging{ appChannel: appChannel, connectionCreatorFn: clientConnFn, appID: appID, @@ -85,7 +87,15 @@ func NewDirectMessaging( hostAddress: hAddr, hostName: hName, maxRequestBodySize: maxRequestBodySize, + proxy: proxy, + } + + if proxy != nil { + proxy.SetRemoteAppFn(dm.getRemoteApp) + proxy.SetTelemetryFn(dm.setContextSpan) } + + return dm } // Invoke takes a message requests and invokes an app, either local or remote. @@ -134,7 +144,7 @@ func (d *directMessaging) invokeWithRetry( code := status.Code(err) if code == codes.Unavailable || code == codes.Unauthenticated { - _, connerr := d.connectionCreatorFn(app.address, app.id, app.namespace, false, true, false) + _, connerr := d.connectionCreatorFn(context.TODO(), app.address, app.id, app.namespace, false, true, false) if connerr != nil { return nil, connerr } @@ -153,14 +163,20 @@ func (d *directMessaging) invokeLocal(ctx context.Context, req *invokev1.InvokeM return d.appChannel.InvokeMethod(ctx, req) } +func (d *directMessaging) setContextSpan(ctx context.Context) context.Context { + span := diag_utils.SpanFromContext(ctx) + ctx = diag.SpanContextToGRPCMetadata(ctx, span.SpanContext()) + + return ctx +} + func (d *directMessaging) invokeRemote(ctx context.Context, appID, namespace, appAddress string, req *invokev1.InvokeMethodRequest) (*invokev1.InvokeMethodResponse, error) { - conn, err := d.connectionCreatorFn(appAddress, appID, namespace, false, false, false) + conn, err := d.connectionCreatorFn(context.TODO(), appAddress, appID, namespace, false, false, false) if err != nil { return nil, err } - span := diag_utils.SpanFromContext(ctx) - ctx = diag.SpanContextToGRPCMetadata(ctx, span.SpanContext()) + ctx = d.setContextSpan(ctx) d.addForwardedHeadersToMetadata(req) d.addDestinationAppIDHeaderToMetadata(appID, req) diff --git a/pkg/messaging/grpc_proxy.go b/pkg/messaging/grpc_proxy.go new file mode 100644 index 00000000000..4dce59448c8 --- /dev/null +++ b/pkg/messaging/grpc_proxy.go @@ -0,0 +1,108 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package messaging + +import ( + "context" + + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + grpc_proxy "github.com/dapr/dapr/pkg/grpc/proxy" + codec "github.com/dapr/dapr/pkg/grpc/proxy/codec" + + "github.com/dapr/dapr/pkg/acl" + "github.com/dapr/dapr/pkg/config" + "github.com/dapr/dapr/pkg/diagnostics" + "github.com/dapr/dapr/pkg/proto/common/v1" +) + +const ( + // GRPCFeatureName is the feature name for the Dapr configuration required to enable the proxy. + GRPCFeatureName = "proxy.grpc" +) + +// Proxy is the interface for a gRPC transparent proxy. +type Proxy interface { + Handler() grpc.StreamHandler + SetRemoteAppFn(func(string) (remoteApp, error)) + SetTelemetryFn(func(context.Context) context.Context) +} + +type proxy struct { + appID string + connectionFactory messageClientConnection + remoteAppFn func(appID string) (remoteApp, error) + remotePort int + telemetryFn func(context.Context) context.Context + localAppAddress string + acl *config.AccessControlList +} + +// NewProxy returns a new proxy. +func NewProxy(connectionFactory messageClientConnection, appID string, localAppAddress string, remoteDaprPort int, acl *config.AccessControlList) Proxy { + return &proxy{ + appID: appID, + connectionFactory: connectionFactory, + localAppAddress: localAppAddress, + remotePort: remoteDaprPort, + acl: acl, + } +} + +// Handler returns a Stream Handler for handling requests that arrive for services that are not recognized by the server. +func (p *proxy) Handler() grpc.StreamHandler { + return grpc_proxy.TransparentHandler(p.intercept) +} + +func (p *proxy) intercept(ctx context.Context, fullName string) (context.Context, *grpc.ClientConn, error) { + md, _ := metadata.FromIncomingContext(ctx) + + v := md.Get(diagnostics.GRPCProxyAppIDKey) + if len(v) == 0 { + return ctx, nil, errors.Errorf("failed to proxy request: required metadata %s not found", diagnostics.GRPCProxyAppIDKey) + } + + outCtx := metadata.NewOutgoingContext(ctx, md.Copy()) + appID := v[0] + + if appID == p.appID { + // proxy locally to the app + if p.acl != nil { + ok, authError := acl.ApplyAccessControlPolicies(ctx, fullName, common.HTTPExtension_NONE, config.GRPCProtocol, p.acl) + if !ok { + return ctx, nil, status.Errorf(codes.PermissionDenied, authError) + } + } + + conn, err := p.connectionFactory(outCtx, p.localAppAddress, p.appID, "", true, false, false, grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name()))) + return ctx, conn, err + } + + // proxy to a remote daprd + remote, err := p.remoteAppFn(appID) + if err != nil { + return ctx, nil, err + } + + conn, err := p.connectionFactory(outCtx, remote.address, remote.id, remote.namespace, false, false, false, grpc.WithDefaultCallOptions(grpc.CallContentSubtype((&codec.Proxy{}).Name()))) + outCtx = p.telemetryFn(outCtx) + + return outCtx, conn, err +} + +// SetRemoteAppFn sets a function that helps the proxy resolve an app ID to an actual address. +func (p *proxy) SetRemoteAppFn(remoteAppFn func(appID string) (remoteApp, error)) { + p.remoteAppFn = remoteAppFn +} + +// SetTelemetryFn sets a function that enriches the context with telemetry. +func (p *proxy) SetTelemetryFn(spanFn func(context.Context) context.Context) { + p.telemetryFn = spanFn +} diff --git a/pkg/messaging/grpc_proxy_test.go b/pkg/messaging/grpc_proxy_test.go new file mode 100644 index 00000000000..b5fdd5b5248 --- /dev/null +++ b/pkg/messaging/grpc_proxy_test.go @@ -0,0 +1,176 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package messaging + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/dapr/dapr/pkg/config" + "github.com/dapr/dapr/pkg/diagnostics" +) + +func connectionFn(ctx context.Context, address, id string, namespace string, skipTLS, recreateIfExists, enableSSL bool, customOpts ...grpc.DialOption) (*grpc.ClientConn, error) { + return grpc.Dial(id, grpc.WithInsecure()) +} + +func TestNewProxy(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + proxy := p.(*proxy) + + assert.Equal(t, "a", proxy.appID) + assert.Equal(t, "a:123", proxy.localAppAddress) + assert.Equal(t, 50005, proxy.remotePort) + assert.NotNil(t, proxy.connectionFactory) +} + +func TestSetRemoteAppFn(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + p.SetRemoteAppFn(func(s string) (remoteApp, error) { + return remoteApp{ + id: "a", + }, nil + }) + + proxy := p.(*proxy) + app, err := proxy.remoteAppFn("a") + + assert.NoError(t, err) + assert.Equal(t, "a", app.id) +} + +func TestSetTelemetryFn(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + p.SetTelemetryFn(func(ctx context.Context) context.Context { + return ctx + }) + + proxy := p.(*proxy) + ctx := metadata.NewOutgoingContext(context.TODO(), metadata.MD{"a": []string{"b"}}) + ctx = proxy.telemetryFn(ctx) + + md, _ := metadata.FromOutgoingContext(ctx) + assert.Equal(t, "b", md["a"][0]) +} + +func TestHandler(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + h := p.Handler() + + assert.NotNil(t, h) +} + +func TestIntercept(t *testing.T) { + t.Run("no app-id in metadata", func(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + p.SetTelemetryFn(func(ctx context.Context) context.Context { + return ctx + }) + + p.SetRemoteAppFn(func(s string) (remoteApp, error) { + return remoteApp{ + id: "a", + }, nil + }) + + ctx := metadata.NewOutgoingContext(context.TODO(), metadata.MD{"a": []string{"b"}}) + proxy := p.(*proxy) + _, conn, err := proxy.intercept(ctx, "/test") + + assert.Error(t, err) + assert.Nil(t, conn) + }) + + t.Run("app-id exists in metadata", func(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + p.SetTelemetryFn(func(ctx context.Context) context.Context { + return ctx + }) + + p.SetRemoteAppFn(func(s string) (remoteApp, error) { + return remoteApp{ + id: "a", + }, nil + }) + + ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD{diagnostics.GRPCProxyAppIDKey: []string{"b"}}) + proxy := p.(*proxy) + _, _, err := proxy.intercept(ctx, "/test") + + assert.NoError(t, err) + }) + + t.Run("proxy to the app", func(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + p.SetTelemetryFn(func(ctx context.Context) context.Context { + return ctx + }) + + p.SetRemoteAppFn(func(s string) (remoteApp, error) { + return remoteApp{ + id: "a", + }, nil + }) + + ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD{diagnostics.GRPCProxyAppIDKey: []string{"a"}}) + proxy := p.(*proxy) + _, conn, err := proxy.intercept(ctx, "/test") + + assert.NoError(t, err) + assert.NotNil(t, conn) + assert.Equal(t, "a", conn.Target()) + }) + + t.Run("proxy to a remote app", func(t *testing.T) { + p := NewProxy(connectionFn, "a", "a:123", 50005, nil) + p.SetTelemetryFn(func(ctx context.Context) context.Context { + ctx = metadata.AppendToOutgoingContext(ctx, "a", "b") + return ctx + }) + + p.SetRemoteAppFn(func(s string) (remoteApp, error) { + return remoteApp{ + id: "b", + }, nil + }) + + ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD{diagnostics.GRPCProxyAppIDKey: []string{"b"}}) + proxy := p.(*proxy) + ctx, conn, err := proxy.intercept(ctx, "/test") + + assert.NoError(t, err) + assert.NotNil(t, conn) + assert.Equal(t, "b", conn.Target()) + + md, _ := metadata.FromOutgoingContext(ctx) + assert.Equal(t, "b", md["a"][0]) + }) + + t.Run("access policies applied", func(t *testing.T) { + acl := &config.AccessControlList{ + DefaultAction: "deny", + TrustDomain: "public", + } + + p := NewProxy(connectionFn, "a", "a:123", 50005, acl) + p.SetTelemetryFn(func(ctx context.Context) context.Context { + ctx = metadata.AppendToOutgoingContext(ctx, "a", "b") + return ctx + }) + + ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD{diagnostics.GRPCProxyAppIDKey: []string{"a"}}) + proxy := p.(*proxy) + + _, conn, err := proxy.intercept(ctx, "/test") + + assert.Error(t, err) + assert.Nil(t, conn) + }) +} diff --git a/pkg/runtime/cli.go b/pkg/runtime/cli.go index cc1e2037d38..5cabc59ec73 100644 --- a/pkg/runtime/cli.go +++ b/pkg/runtime/cli.go @@ -16,6 +16,7 @@ import ( "github.com/dapr/kit/logger" + "github.com/dapr/dapr/pkg/acl" global_config "github.com/dapr/dapr/pkg/config" env "github.com/dapr/dapr/pkg/config/env" "github.com/dapr/dapr/pkg/cors" @@ -212,7 +213,7 @@ func FromFlags() (*DaprRuntime, error) { globalConfig = global_config.LoadDefaultConfiguration() } - accessControlList, err = global_config.ParseAccessControlSpec(globalConfig.Spec.AccessControlSpec, string(runtimeConfig.ApplicationProtocol)) + accessControlList, err = acl.ParseAccessControlSpec(globalConfig.Spec.AccessControlSpec, string(runtimeConfig.ApplicationProtocol)) if err != nil { log.Fatalf(err.Error()) } diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index c82827706bb..7c7a0829ddb 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -155,6 +155,8 @@ type DaprRuntime struct { pendingComponents chan components_v1alpha1.Component pendingComponentDependents map[string][]components_v1alpha1.Component + + proxy messaging.Proxy } type componentPreprocessRes struct { @@ -322,6 +324,10 @@ func (a *DaprRuntime) initRuntime(opts *runtimeOpts) error { // Setup allow/deny list for secrets a.populateSecretsConfiguration() + + // Start proxy + a.initProxy() + // Create and start internal and external gRPC servers grpcAPI := a.getGRPCAPI() @@ -510,7 +516,18 @@ func (a *DaprRuntime) initDirectMessaging(resolver nr.Resolver) { a.grpc.GetGRPCConnection, resolver, a.globalConfig.Spec.TracingSpec, - a.runtimeConfig.MaxRequestBodySize) + a.runtimeConfig.MaxRequestBodySize, + a.proxy) +} + +func (a *DaprRuntime) initProxy() { + //TODO: remove feature check once stable + if config.IsFeatureEnabled(a.globalConfig.Spec.Features, messaging.GRPCFeatureName) { + a.proxy = messaging.NewProxy(a.grpc.GetGRPCConnection, a.runtimeConfig.ID, + fmt.Sprintf("%s:%d", channel.DefaultChannelAddress, a.runtimeConfig.ApplicationPort), a.runtimeConfig.InternalGRPCPort, a.accessControlList) + + log.Info("gRPC proxy enabled") + } } func (a *DaprRuntime) beginComponentsUpdates() error { @@ -767,14 +784,14 @@ func (a *DaprRuntime) startHTTPServer(port, profilePort int, allowedOrigins stri func (a *DaprRuntime) startGRPCInternalServer(api grpc.API, port int) error { serverConf := a.getNewServerConfig(port) - server := grpc.NewInternalServer(api, serverConf, a.globalConfig.Spec.TracingSpec, a.globalConfig.Spec.MetricSpec, a.authenticator) + server := grpc.NewInternalServer(api, serverConf, a.globalConfig.Spec.TracingSpec, a.globalConfig.Spec.MetricSpec, a.authenticator, a.proxy) err := server.StartNonBlocking() return err } func (a *DaprRuntime) startGRPCAPIServer(api grpc.API, port int) error { serverConf := a.getNewServerConfig(port) - server := grpc.NewAPIServer(api, serverConf, a.globalConfig.Spec.TracingSpec, a.globalConfig.Spec.MetricSpec, a.globalConfig.Spec.APISpec) + server := grpc.NewAPIServer(api, serverConf, a.globalConfig.Spec.TracingSpec, a.globalConfig.Spec.MetricSpec, a.globalConfig.Spec.APISpec, a.proxy) err := server.StartNonBlocking() return err } diff --git a/tests/apps/service_invocation_grpc_proxy_client/Dockerfile b/tests/apps/service_invocation_grpc_proxy_client/Dockerfile new file mode 100644 index 00000000000..847e693d0bc --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_client/Dockerfile @@ -0,0 +1,16 @@ +# ------------------------------------------------------------ +# Copyright (c) Microsoft Corporation and Dapr Contributors. +# Licensed under the MIT License. +# ------------------------------------------------------------ + +FROM golang:1.16 as build_env + +WORKDIR /app +COPY . . +RUN go get -d -v +RUN go build -o app . + +FROM debian:buster-slim +WORKDIR / +COPY --from=build_env /app/app / +CMD ["/app"] diff --git a/tests/apps/service_invocation_grpc_proxy_client/Dockerfile-windows b/tests/apps/service_invocation_grpc_proxy_client/Dockerfile-windows new file mode 100644 index 00000000000..55ebe9c81a1 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_client/Dockerfile-windows @@ -0,0 +1,13 @@ +# ------------------------------------------------------------ +# Copyright (c) Microsoft Corporation and Dapr Contributors. +# Licensed under the MIT License. +# ------------------------------------------------------------ + +FROM golang:1.16-nanoserver + +WORKDIR /app +COPY . . +RUN go get -d -v +RUN go build -o app.exe . + +CMD ["app.exe"] diff --git a/tests/apps/service_invocation_grpc_proxy_client/app.go b/tests/apps/service_invocation_grpc_proxy_client/app.go new file mode 100644 index 00000000000..ecd17d9cc36 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_client/app.go @@ -0,0 +1,63 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/helloworld/helloworld" + "google.golang.org/grpc/metadata" +) + +type appResponse struct { + Message string `json:"message,omitempty"` +} + +func run(w http.ResponseWriter, r *http.Request) { + conn, err := grpc.Dial("localhost:50001", grpc.WithInsecure(), grpc.WithBlock()) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() + c := pb.NewGreeterClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + + ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "grpcproxyserver") + resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Darth Tyranus"}) + if err != nil { + log.Printf("could not greet: %v\n", err) + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("failed to proxy request: %s", err))) + return + } + + log.Printf("Greeting: %s", resp.GetMessage()) + + appResp := appResponse{ + Message: "success", + } + + b, err := json.Marshal(appResp) + if err != nil { + log.Fatal(err) + } + + w.WriteHeader(200) + w.Write(b) +} + +func main() { + http.HandleFunc("/tests/invoke_test", run) + log.Fatal(http.ListenAndServe(":3000", nil)) +} diff --git a/tests/apps/service_invocation_grpc_proxy_client/go.mod b/tests/apps/service_invocation_grpc_proxy_client/go.mod new file mode 100644 index 00000000000..6d629e41186 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_client/go.mod @@ -0,0 +1,8 @@ +module app + +go 1.16 + +require ( + google.golang.org/grpc v1.38.0 + google.golang.org/grpc/examples v0.0.0-20210610163306-6351a55c3895 +) diff --git a/tests/apps/service_invocation_grpc_proxy_client/go.sum b/tests/apps/service_invocation_grpc_proxy_client/go.sum new file mode 100644 index 00000000000..f0b40d48642 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_client/go.sum @@ -0,0 +1,95 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc/examples v0.0.0-20210610163306-6351a55c3895 h1:gkOGw8P+cjlOkwkqO/syZdUKX3GH+JzYbtYClNsSnkA= +google.golang.org/grpc/examples v0.0.0-20210610163306-6351a55c3895/go.mod h1:bF8wuZSAZTcbF7ZPKrDI/qY52toTP/yxLpRRY4Eu9Js= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tests/apps/service_invocation_grpc_proxy_client/service.yaml b/tests/apps/service_invocation_grpc_proxy_client/service.yaml new file mode 100644 index 00000000000..6ef933e16ab --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_client/service.yaml @@ -0,0 +1,45 @@ +# In e2e test, this will not be used to deploy the app to test cluster. +# This is created for testing purpose in order to deploy this app using kubectl +# before writing e2e test. +kind: Service +apiVersion: v1 +metadata: + name: service-invocation-grpc-proxy-client + labels: + testapp: service-invocation-grpc-proxy-client +spec: + selector: + testapp: service-invocation-grpc-proxy-client + ports: + - protocol: TCP + port: 80 + targetPort: 3000 + type: LoadBalancer + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: service-invocation-grpc-proxy-client + labels: + testapp: service-invocation-grpc-proxy-client +spec: + replicas: 1 + selector: + matchLabels: + testapp: service-invocation-grpc-proxy-client + template: + metadata: + labels: + testapp: service-invocation-grpc-proxy-client + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "grpcproxyclient" + spec: + containers: + - name: service-invocation-grpc-proxy-client + image: docker.io/[YOUR ALIAS]/e2e-service_invocation_grpc_proyx_client:dev + ports: + - containerPort: 50051 + - containerPort: 3000 + imagePullPolicy: Always diff --git a/tests/apps/service_invocation_grpc_proxy_server/Dockerfile b/tests/apps/service_invocation_grpc_proxy_server/Dockerfile new file mode 100644 index 00000000000..847e693d0bc --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_server/Dockerfile @@ -0,0 +1,16 @@ +# ------------------------------------------------------------ +# Copyright (c) Microsoft Corporation and Dapr Contributors. +# Licensed under the MIT License. +# ------------------------------------------------------------ + +FROM golang:1.16 as build_env + +WORKDIR /app +COPY . . +RUN go get -d -v +RUN go build -o app . + +FROM debian:buster-slim +WORKDIR / +COPY --from=build_env /app/app / +CMD ["/app"] diff --git a/tests/apps/service_invocation_grpc_proxy_server/Dockerfile-windows b/tests/apps/service_invocation_grpc_proxy_server/Dockerfile-windows new file mode 100644 index 00000000000..55ebe9c81a1 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_server/Dockerfile-windows @@ -0,0 +1,13 @@ +# ------------------------------------------------------------ +# Copyright (c) Microsoft Corporation and Dapr Contributors. +# Licensed under the MIT License. +# ------------------------------------------------------------ + +FROM golang:1.16-nanoserver + +WORKDIR /app +COPY . . +RUN go get -d -v +RUN go build -o app.exe . + +CMD ["app.exe"] diff --git a/tests/apps/service_invocation_grpc_proxy_server/app.go b/tests/apps/service_invocation_grpc_proxy_server/app.go new file mode 100644 index 00000000000..930f2f10212 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_server/app.go @@ -0,0 +1,43 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation and Dapr Contributors. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package main + +import ( + "context" + "log" + "net" + + "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/helloworld/helloworld" +) + +const ( + port = ":50051" +) + +// server is used to implement helloworld.GreeterServer. +type server struct { + pb.UnimplementedGreeterServer +} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + log.Printf("Received: %v", in.GetName()) + return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil +} + +func main() { + lis, err := net.Listen("tcp", port) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterGreeterServer(s, &server{}) + log.Printf("server listening at %v", lis.Addr()) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/tests/apps/service_invocation_grpc_proxy_server/go.mod b/tests/apps/service_invocation_grpc_proxy_server/go.mod new file mode 100644 index 00000000000..6d629e41186 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_server/go.mod @@ -0,0 +1,8 @@ +module app + +go 1.16 + +require ( + google.golang.org/grpc v1.38.0 + google.golang.org/grpc/examples v0.0.0-20210610163306-6351a55c3895 +) diff --git a/tests/apps/service_invocation_grpc_proxy_server/go.sum b/tests/apps/service_invocation_grpc_proxy_server/go.sum new file mode 100644 index 00000000000..f0b40d48642 --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_server/go.sum @@ -0,0 +1,95 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc/examples v0.0.0-20210610163306-6351a55c3895 h1:gkOGw8P+cjlOkwkqO/syZdUKX3GH+JzYbtYClNsSnkA= +google.golang.org/grpc/examples v0.0.0-20210610163306-6351a55c3895/go.mod h1:bF8wuZSAZTcbF7ZPKrDI/qY52toTP/yxLpRRY4Eu9Js= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tests/apps/service_invocation_grpc_proxy_server/service.yaml b/tests/apps/service_invocation_grpc_proxy_server/service.yaml new file mode 100644 index 00000000000..a94a17ec3ac --- /dev/null +++ b/tests/apps/service_invocation_grpc_proxy_server/service.yaml @@ -0,0 +1,30 @@ +# In e2e test, this will not be used to deploy the app to test cluster. +# This is created for testing purpose in order to deploy this app using kubectl +# before writing e2e test. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: service-invocation-grpc-proxy-server + labels: + testapp: service-invocation-grpc-proxy-server +spec: + replicas: 1 + selector: + matchLabels: + testapp: service-invocation-grpc-proxy-server + template: + metadata: + labels: + testapp: service-invocation-grpc-proxy-server + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "grpcproxyserver" + dapr.io/app-port: "50051" + dapr.io/app-protocol: "grpc" + spec: + containers: + - name: service-invocation-grpc-proxy-server + image: docker.io/[YOUR ALIAS]/e2e-service_invocation_grpc_proxy_server:dev + ports: + - containerPort: 50051 + imagePullPolicy: Always diff --git a/tests/config/kubernetes_grpc_proxy_config.yaml b/tests/config/kubernetes_grpc_proxy_config.yaml new file mode 100644 index 00000000000..b759c0781ae --- /dev/null +++ b/tests/config/kubernetes_grpc_proxy_config.yaml @@ -0,0 +1,8 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: grpcproxyconfig +spec: + features: + - name: proxy.grpc + enabled: true diff --git a/tests/dapr_tests.mk b/tests/dapr_tests.mk index e427e23abc6..b38b7a2a182 100644 --- a/tests/dapr_tests.mk +++ b/tests/dapr_tests.mk @@ -14,6 +14,8 @@ stateapp \ secretapp \ service_invocation \ service_invocation_grpc \ +service_invocation_grpc_proxy_client \ +service_invocation_grpc_proxy_server \ binding_input \ binding_input_grpc \ binding_output \ @@ -28,7 +30,7 @@ actorreentrancy \ runtime \ runtime_init \ middleware \ -job-publisher +job-publisher \ # PERFORMANCE test app list PERF_TEST_APPS=actorfeatures actorjava tester service_invocation_http @@ -241,6 +243,7 @@ setup-test-components: setup-app-configurations $(KUBECTL) apply -f ./tests/config/uppercase.yaml --namespace $(DAPR_TEST_NAMESPACE) $(KUBECTL) apply -f ./tests/config/pipeline.yaml --namespace $(DAPR_TEST_NAMESPACE) $(KUBECTL) apply -f ./tests/config/app_reentrant_actor.yaml --namespace $(DAPR_TEST_NAMESPACE) + $(KUBECTL) apply -f ./tests/config/kubernetes_grpc_proxy_config.yaml --namespace $(DAPR_TEST_NAMESPACE) # Show the installed components $(KUBECTL) get components --namespace $(DAPR_TEST_NAMESPACE) diff --git a/tests/e2e/service_invocation/service_invocation_test.go b/tests/e2e/service_invocation/service_invocation_test.go index d4131e51155..6b43a6f07e6 100644 --- a/tests/e2e/service_invocation/service_invocation_test.go +++ b/tests/e2e/service_invocation/service_invocation_test.go @@ -111,6 +111,26 @@ func TestMain(m *testing.M) { Namespace: &secondaryNamespace, AppProtocol: "grpc", }, + { + AppName: "grpcproxyclient", + DaprEnabled: true, + ImageName: "e2e-service_invocation_grpc_proxy_client", + Replicas: 1, + IngressEnabled: true, + MetricsEnabled: true, + Config: "grpcproxyconfig", + }, + { + AppName: "grpcproxyserver", + DaprEnabled: true, + ImageName: "e2e-service_invocation_grpc_proxy_server", + Replicas: 1, + IngressEnabled: false, + MetricsEnabled: true, + AppProtocol: "grpc", + Config: "grpcproxyconfig", + AppPort: 50051, + }, } tr = runner.NewTestRunner("hellodapr", testApps, nil, nil) @@ -209,6 +229,20 @@ var crossNamespaceTests = []struct { }, } +var grpcProxyTests = []struct { + in string + remoteApp string + appMethod string + expectedResponse string +}{ + { + "Test grpc proxy", + "grpcproxyclient", + "", + "success", + }, +} + func TestServiceInvocation(t *testing.T) { externalURL := tr.Platform.AcquireAppExternalURL("serviceinvocation-caller") require.NotEmpty(t, externalURL, "external URL must not be empty!") @@ -268,6 +302,39 @@ func TestServiceInvocation(t *testing.T) { } } +func TestGRPCProxy(t *testing.T) { + externalURL := tr.Platform.AcquireAppExternalURL("grpcproxyclient") + require.NotEmpty(t, externalURL, "external URL must not be empty!") + var err error + // This initial probe makes the test wait a little bit longer when needed, + // making this test less flaky due to delays in the deployment. + _, err = utils.HTTPGetNTimes(externalURL, numHealthChecks) + require.NoError(t, err) + + t.Logf("externalURL is '%s'\n", externalURL) + + for _, tt := range grpcProxyTests { + t.Run(tt.in, func(t *testing.T) { + body, err := json.Marshal(testCommandRequest{ + RemoteApp: tt.remoteApp, + Method: tt.appMethod, + }) + require.NoError(t, err) + + resp, err := utils.HTTPPost( + fmt.Sprintf("%s/tests/invoke_test", externalURL), body) + t.Log("checking err...") + require.NoError(t, err) + + var appResp appResponse + t.Logf("unmarshalling..%s\n", string(resp)) + err = json.Unmarshal(resp, &appResp) + require.NoError(t, err) + require.Equal(t, tt.expectedResponse, appResp.Message) + }) + } +} + func TestHeaders(t *testing.T) { externalURL := tr.Platform.AcquireAppExternalURL("serviceinvocation-caller") require.NotEmpty(t, externalURL, "external URL must not be empty!")