diff --git a/api/go.mod b/api/go.mod index 17022dc686..7f70d44a16 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.21.5 require ( - github.com/hashicorp/boundary/sdk v0.0.40 + github.com/hashicorp/boundary/sdk v0.0.48 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-kms-wrapping/v2 v2.0.14 github.com/hashicorp/go-retryablehttp v0.7.4 @@ -20,7 +20,7 @@ require ( github.com/stretchr/testify v1.8.4 go.uber.org/atomic v1.11.0 golang.org/x/time v0.3.0 - google.golang.org/grpc v1.59.0 + google.golang.org/grpc v1.61.0 google.golang.org/protobuf v1.33.0 nhooyr.io/websocket v1.8.10 ) @@ -38,9 +38,10 @@ require ( github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sys v0.16.0 // indirect + google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/api/go.sum b/api/go.sum index 6600349d5b..4b7b62d724 100644 --- a/api/go.sum +++ b/api/go.sum @@ -21,12 +21,12 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/boundary/sdk v0.0.40 h1:HNJcMWHCjoraPJALTZ9JssSoP/vflew2+VB656nvRlY= -github.com/hashicorp/boundary/sdk v0.0.40/go.mod h1:+XTDYf9YNeKIbGOPJwy7hlO2Le4zgzCtHCG/u+z4THI= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/boundary/sdk v0.0.48 h1:4HqyX1tS1kuaCa18OSbPGf8ZHJuwdmm1yaxr1u+nxZ4= +github.com/hashicorp/boundary/sdk v0.0.48/go.mod h1:9iOT7kDM6mYcSkKxNuZlv8rP7U5BG1kXoevjLLL8lNQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -88,8 +88,8 @@ github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -117,14 +117,13 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -146,8 +145,8 @@ go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -157,19 +156,19 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -182,22 +181,22 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= -google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 h1:HJMDndgxest5n2y77fnErkM62iUsptE/H8p0dC2Huo4= -google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index 1b0b9891a8..1475a9610d 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 github.com/hashicorp/boundary/api v0.0.50 - github.com/hashicorp/boundary/sdk v0.0.46 + github.com/hashicorp/boundary/sdk v0.0.48 github.com/hashicorp/cap v0.5.1-0.20240315182732-faa330bfb8df github.com/hashicorp/dawdle v0.5.0 github.com/hashicorp/eventlogger v0.2.9 diff --git a/internal/alias/target/repository_alias_list_resolvable.go b/internal/alias/target/repository_alias_list_resolvable.go index da78507eaf..bb7a39bec0 100644 --- a/internal/alias/target/repository_alias_list_resolvable.go +++ b/internal/alias/target/repository_alias_list_resolvable.go @@ -10,26 +10,57 @@ import ( "strings" "time" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/types/scope" ) -// targetAndScopeIdsForDestinations returns the target ids for which there is -// at least one permission. If all targets in a specific scope are granted -// permission for an action, then the scope id is in the returned scope id slice. -func targetAndScopeIdsForDestinations(perms []perms.Permission) ([]string, []string) { - var targetIds, scopeIds []string - for _, perm := range perms { +func splitPermissions(permissions []perms.Permission) (directIds, directScopeIds, childAllScopes []string, allDescendants bool) { + // First check for all descendants. Since what we are querying for below is + // for targets (either IDs, or targets within specific scopes), and targets + // are not in global, if this matches we can actually ignore everything + // else. + for _, perm := range permissions { + if perm.GrantScopeId == globals.GrantScopeDescendants && perm.All { + allDescendants = true + return + } + } + + directIds = make([]string, 0, len(permissions)) + directScopeIds = make([]string, 0, len(permissions)) + childAllScopes = make([]string, 0, len(permissions)) + for _, perm := range permissions { switch { + case allDescendants: + // See the above check; we don't need any other info + case perm.GrantScopeId == scope.Global.String() || strings.HasPrefix(perm.GrantScopeId, globals.OrgPrefix): + // There are no targets in global or orgs + case perm.RoleScopeId == scope.Global.String() && perm.GrantScopeId == globals.GrantScopeChildren: + // A role in global that includes children will include only orgs, + // which do not have targets, so ignore + case perm.GrantScopeId == globals.GrantScopeChildren && perm.All: + // Because of the above check this will match only grants from org + // roles. If the grant scope is children and all, we store the scope + // ID. + childAllScopes = append(childAllScopes, perm.RoleScopeId) case perm.All: - scopeIds = append(scopeIds, perm.ScopeId) + // We ignore descendants and if this was a children grant scope and + // perm.All it would match the above case. So this is a grant + // directly on a scope. Since only projects contain targets, we can + // ignore any grant scope ID that doesn't match targets. + if strings.HasPrefix(perm.GrantScopeId, globals.ProjectPrefix) { + directScopeIds = append(directScopeIds, perm.GrantScopeId) + } case len(perm.ResourceIds) > 0: - targetIds = append(targetIds, perm.ResourceIds...) + // It's an ID grant + directIds = append(directIds, perm.ResourceIds...) } } - return targetIds, scopeIds + return } // listResolvableAliases lists aliases which have a destination id set to that @@ -41,7 +72,8 @@ func (r *Repository) listResolvableAliases(ctx context.Context, permissions []pe case len(permissions) == 0: return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") } - toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions) + + directIds, directScopeIds, childAllScopes, allDescendants := splitPermissions(permissions) opts, err := getOpts(opt...) if err != nil { @@ -59,16 +91,33 @@ func (r *Repository) listResolvableAliases(ctx context.Context, permissions []pe var args []any var destinationIdClauses []string - if len(toTargetIds) > 0 { - destinationIdClauses = append(destinationIdClauses, "destination_id in @target_ids") - args = append(args, sql.Named("target_ids", toTargetIds)) - } - if len(toTargetsInScopeIds) > 0 { - destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id in @target_scope_ids)") - args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds)) - } - if len(destinationIdClauses) == 0 { - return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + + switch { + case allDescendants: + // This matches all targets + destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target)") + default: + // Add orgs with all permissions on children + if len(childAllScopes) > 0 { + destinationIdClauses = append(destinationIdClauses, + "destination_id in "+ + "(select public_id from target where project_id in "+ + "(select public_id from iam_scope where parent_id = any(@child_all_scopes)))", + ) + args = append(args, sql.Named("child_all_scopes", "{"+strings.Join(childAllScopes, ",")+"}")) + } + // Add target ids + if len(directIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id = any(@target_ids)") + args = append(args, sql.Named("target_ids", "{"+strings.Join(directIds, ",")+"}")) + } + if len(directScopeIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id = any(@target_scope_ids))") + args = append(args, sql.Named("target_scope_ids", "{"+strings.Join(directScopeIds, ",")+"}")) + } + if len(destinationIdClauses) == 0 && len(childAllScopes) == 0 { + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + } } whereClause := fmt.Sprintf("destination_id is not null and (%s)", strings.Join(destinationIdClauses, " or ")) @@ -98,7 +147,8 @@ func (r *Repository) listResolvableAliasesRefresh(ctx context.Context, updatedAf case len(permissions) == 0: return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") } - toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions) + + directIds, directScopeIds, childAllScopes, allDescendants := splitPermissions(permissions) opts, err := getOpts(opt...) if err != nil { @@ -116,16 +166,33 @@ func (r *Repository) listResolvableAliasesRefresh(ctx context.Context, updatedAf var args []any var destinationIdClauses []string - if len(toTargetIds) > 0 { - destinationIdClauses = append(destinationIdClauses, "destination_id in @target_ids") - args = append(args, sql.Named("target_ids", toTargetIds)) - } - if len(toTargetsInScopeIds) > 0 { - destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id in @target_scope_ids)") - args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds)) - } - if len(destinationIdClauses) == 0 { - return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + + switch { + case allDescendants: + // This matches all targets + destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target)") + default: + // Add orgs with all permissions on children + if len(childAllScopes) > 0 { + destinationIdClauses = append(destinationIdClauses, + "destination_id in "+ + "(select public_id from target where project_id in "+ + "(select public_id from iam_scope where parent_id = any(@child_all_scopes)))", + ) + args = append(args, sql.Named("child_all_scopes", "{"+strings.Join(childAllScopes, ",")+"}")) + } + // Add target ids + if len(directIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id = any(@target_ids)") + args = append(args, sql.Named("target_ids", "{"+strings.Join(directIds, ",")+"}")) + } + if len(directScopeIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id in (select public_id from target where project_id = any(@target_scope_ids))") + args = append(args, sql.Named("target_scope_ids", "{"+strings.Join(directScopeIds, ",")+"}")) + } + if len(destinationIdClauses) == 0 && len(childAllScopes) == 0 { + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + } } whereClause := fmt.Sprintf("update_time > @updated_after_time and destination_id is not null and (%s)", @@ -162,21 +229,40 @@ func (r *Repository) listRemovedResolvableAliasIds(ctx context.Context, since ti // to be provided. return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing permissions") } - toTargetIds, toTargetsInScopeIds := targetAndScopeIdsForDestinations(permissions) + + directIds, directScopeIds, childAllScopes, allDescendants := splitPermissions(permissions) var args []any var destinationIdClauses []string - if len(toTargetIds) > 0 { - destinationIdClauses = append(destinationIdClauses, "destination_id not in @target_ids") - args = append(args, sql.Named("target_ids", toTargetIds)) - } - if len(toTargetsInScopeIds) > 0 { - destinationIdClauses = append(destinationIdClauses, "destination_id not in (select public_id from target where project_id in @target_scope_ids)") - args = append(args, sql.Named("target_scope_ids", toTargetsInScopeIds)) - } - if len(destinationIdClauses) == 0 { - return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + + switch { + case allDescendants: + // This matches all targets + destinationIdClauses = append(destinationIdClauses, "destination_id not in (select public_id from target)") + default: + // Add orgs with all permissions on children + if len(childAllScopes) > 0 { + destinationIdClauses = append(destinationIdClauses, + "destination_id not in "+ + "(select public_id from target where project_id in "+ + "(select public_id from iam_scope where parent_id = any(@child_all_scopes)))", + ) + args = append(args, sql.Named("child_all_scopes", "{"+strings.Join(childAllScopes, ",")+"}")) + } + // Add target ids + if len(directIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id != all(@target_ids)") + args = append(args, sql.Named("target_ids", "{"+strings.Join(directIds, ",")+"}")) + } + if len(directScopeIds) > 0 { + destinationIdClauses = append(destinationIdClauses, "destination_id not in (select public_id from target where project_id = any(@target_scope_ids))") + args = append(args, sql.Named("target_scope_ids", "{"+strings.Join(directScopeIds, ",")+"}")) + } + if len(destinationIdClauses) == 0 && len(childAllScopes) == 0 { + return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "no target ids or scope ids provided") + } } + whereClause := fmt.Sprintf("update_time > @updated_after_time and (destination_id is null or (%s))", strings.Join(destinationIdClauses, " and ")) args = append(args, diff --git a/internal/alias/target/service_list_resolvable_ext_test.go b/internal/alias/target/service_list_resolvable_ext_test.go index 86a53bb15d..48d6be51c3 100644 --- a/internal/alias/target/service_list_resolvable_ext_test.go +++ b/internal/alias/target/service_list_resolvable_ext_test.go @@ -30,6 +30,11 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +// NOTE: These tests rely on state from previous tests, so they should be run in +// order -- running some subtests on their own will result in errors. It might +// be nice for them to be refactored at some point to start with a known state +// to avoid this. At the time I'm writing this, I'm not doing that because I'm +// not sure if this was a purposeful design choice. func TestService_ListResolvableAliases(t *testing.T) { fiveDaysAgo := time.Now() // Set database read timeout to avoid duplicates in response @@ -56,12 +61,12 @@ func TestService_ListResolvableAliases(t *testing.T) { } byIdPerms := []perms.Permission{ { - ScopeId: proj.GetPublicId(), - Resource: resource.Target, - Action: action.ListResolvableAliases, - ResourceIds: []string{tar.GetPublicId(), "ttcp_unknownid"}, - OnlySelf: false, - All: false, + GrantScopeId: proj.GetPublicId(), + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{tar.GetPublicId(), "ttcp_unknownid"}, + OnlySelf: false, + All: false, }, } // Reverse since we read items in descending order (newest first) @@ -76,16 +81,48 @@ func TestService_ListResolvableAliases(t *testing.T) { } byScopePerms := []perms.Permission{ { - ScopeId: proj2.GetPublicId(), - Resource: resource.Target, - Action: action.ListResolvableAliases, - OnlySelf: false, - All: true, + GrantScopeId: proj2.GetPublicId(), + Resource: resource.Target, + Action: action.ListResolvableAliases, + OnlySelf: false, + All: true, }, } // Reverse since we read items in descending order (newest first) slices.Reverse(byScopeResources) + org3, proj3 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + tar3 := tcp.TestTarget(ctx, t, conn, proj3.GetPublicId(), "target3") + var byChildrenResources []*target.Alias + for i := 0; i < 5; i++ { + r := target.TestAlias(t, rw, fmt.Sprintf("test%d.alias.by-children", i), target.WithDestinationId(tar3.GetPublicId())) + byChildrenResources = append(byChildrenResources, r) + } + byChildrenPerms := []perms.Permission{ + { + RoleScopeId: org3.GetPublicId(), + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Resource: resource.Target, + Action: action.ListResolvableAliases, + OnlySelf: false, + All: true, + }, + } + // Reverse since we read items in descending order (newest first) + slices.Reverse(byChildrenResources) + + byDescendantsPerms := []perms.Permission{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Resource: resource.Target, + Action: action.ListResolvableAliases, + OnlySelf: false, + All: true, + }, + } + repo, repoErr := target.NewRepository(ctx, rw, rw, kmsCache) require.NoError(t, repoErr) @@ -296,21 +333,41 @@ func TestService_ListResolvableAliases(t *testing.T) { }) }) + // Build the descendants resources for the first tests + byDescendantsResources := append([]*target.Alias{}, byChildrenResources...) + byDescendantsResources = append(byDescendantsResources, byScopeResources...) + byDescendantsResources = append(byDescendantsResources, byIdResources...) + t.Run("simple pagination", func(t *testing.T) { cases := []struct { name string perms []perms.Permission resourceSlice []*target.Alias + lastPageSize int }{ { name: "by-id", perms: byIdPerms, resourceSlice: byIdResources, + lastPageSize: 1, }, { name: "by-scope", perms: byScopePerms, resourceSlice: byScopeResources, + lastPageSize: 1, + }, + { + name: "by-children", + perms: byChildrenPerms, + resourceSlice: byChildrenResources, + lastPageSize: 1, + }, + { + name: "by-descendants", + perms: byDescendantsPerms, + resourceSlice: byDescendantsResources, + lastPageSize: 11, }, } for _, tc := range cases { @@ -320,7 +377,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp.CompleteListing) - require.Equal(t, resp.EstimatedItemCount, 10) + require.Equal(t, 15, resp.EstimatedItemCount) require.Empty(t, resp.DeletedIds) require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], tc.resourceSlice[0], cmpIgnoreUnexportedOpts), "resources did not match", tc.resourceSlice, "resp", resp.Items) @@ -329,7 +386,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp2.CompleteListing) - require.Equal(t, resp2.EstimatedItemCount, 10) + require.Equal(t, 15, resp2.EstimatedItemCount) require.Empty(t, resp2.DeletedIds) require.Len(t, resp2.Items, 1) require.Empty(t, cmp.Diff(resp2.Items[0], tc.resourceSlice[1], cmpIgnoreUnexportedOpts)) @@ -338,7 +395,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 10) + require.Equal(t, 15, resp3.EstimatedItemCount) require.Empty(t, resp3.DeletedIds) require.Len(t, resp3.Items, 1) require.Empty(t, cmp.Diff(resp3.Items[0], tc.resourceSlice[2], cmpIgnoreUnexportedOpts)) @@ -347,18 +404,18 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp4.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp4.CompleteListing) - require.Equal(t, resp4.EstimatedItemCount, 10) + require.Equal(t, 15, resp4.EstimatedItemCount) require.Empty(t, resp4.DeletedIds) require.Len(t, resp4.Items, 1) require.Empty(t, cmp.Diff(resp4.Items[0], tc.resourceSlice[3], cmpIgnoreUnexportedOpts)) - resp5, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 1, resp4.ListToken, repo, tc.perms) + resp5, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), tc.lastPageSize, resp4.ListToken, repo, tc.perms) require.NoError(t, err) require.Equal(t, resp5.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp5.CompleteListing) - require.Equal(t, resp5.EstimatedItemCount, 10) + require.Equal(t, 15, resp5.EstimatedItemCount) require.Empty(t, resp5.DeletedIds) - require.Len(t, resp5.Items, 1) + require.Len(t, resp5.Items, tc.lastPageSize) require.Empty(t, cmp.Diff(resp5.Items[0], tc.resourceSlice[4], cmpIgnoreUnexportedOpts)) // Finished initial pagination phase, request refresh @@ -367,7 +424,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp6.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp6.CompleteListing) - require.Equal(t, resp6.EstimatedItemCount, 10) + require.Equal(t, 15, resp6.EstimatedItemCount) require.Empty(t, resp6.DeletedIds) require.Empty(t, resp6.Items) @@ -392,7 +449,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp7.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp7.CompleteListing) - require.Equal(t, resp7.EstimatedItemCount, 12) + require.Equal(t, 17, resp7.EstimatedItemCount) require.Empty(t, resp7.DeletedIds) require.Len(t, resp7.Items, 1) require.Empty(t, cmp.Diff(resp7.Items[0], newR2, cmpIgnoreUnexportedOpts)) @@ -402,7 +459,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp8.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp8.CompleteListing) - require.Equal(t, resp8.EstimatedItemCount, 12) + require.Equal(t, 17, resp8.EstimatedItemCount) require.Empty(t, resp8.DeletedIds) require.Len(t, resp8.Items, 1) require.Empty(t, cmp.Diff(resp8.Items[0], newR1, cmpIgnoreUnexportedOpts)) @@ -412,14 +469,14 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp9.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp9.CompleteListing) - require.Equal(t, resp9.EstimatedItemCount, 12) + require.Equal(t, 17, resp9.EstimatedItemCount) require.Empty(t, resp9.DeletedIds) require.Empty(t, resp9.Items) }) } }) - t.Run("simple pagination with destination id changes", func(t *testing.T) { + t.Run("simple pagination with destination id changes - id", func(t *testing.T) { firstUpdatedA := byScopeResources[0] // this no longer has the destination id that has permissions firstUpdatedA.DestinationId = tar.GetPublicId() @@ -442,7 +499,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp.CompleteListing) - require.Equal(t, resp.EstimatedItemCount, 10) + require.Equal(t, resp.EstimatedItemCount, 15) require.Empty(t, resp.DeletedIds) require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], byScopeResources[0], cmpIgnoreUnexportedOpts)) @@ -452,7 +509,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp2.CompleteListing) - require.Equal(t, resp2.EstimatedItemCount, 10) + require.Equal(t, resp2.EstimatedItemCount, 15) require.Empty(t, resp2.DeletedIds) require.Len(t, resp2.Items, 3) require.Empty(t, cmp.Diff(resp2.Items, byScopeResources[1:], cmpIgnoreUnexportedOpts)) @@ -479,12 +536,99 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 10) + require.Equal(t, resp3.EstimatedItemCount, 15) require.Contains(t, resp3.DeletedIds, secondA.GetPublicId()) require.Empty(t, resp3.Items) }) - t.Run("simple pagination with deletion", func(t *testing.T) { + t.Run("simple pagination with destination id changes - children", func(t *testing.T) { + firstUpdatedA := byChildrenResources[0] + // this no longer has the destination id that has permissions + firstUpdatedA.DestinationId = tar.GetPublicId() + firstUpdatedA, _, err := repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byChildrenResources = byChildrenResources[1:] + t.Cleanup(func() { + firstUpdatedA.DestinationId = tar3.GetPublicId() + firstUpdatedA, _, err := repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byChildrenResources = append([]*target.Alias{firstUpdatedA}, byChildrenResources...) + }) + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, byChildrenPerms) + require.NoError(t, err) + require.NotNil(t, resp.ListToken) + require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp.CompleteListing) + require.Equal(t, resp.EstimatedItemCount, 15) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byChildrenResources[0], cmpIgnoreUnexportedOpts)) + + // request remaining results + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 3, resp.ListToken, repo, byChildrenPerms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp2.CompleteListing) + require.Equal(t, resp2.EstimatedItemCount, 15) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 3) + require.Empty(t, cmp.Diff(resp2.Items, byChildrenResources[1:], cmpIgnoreUnexportedOpts)) + }) + + // We have to re-build the expected set of resources as the original slices + // have been updated with updated values + byDescendantsResources = append([]*target.Alias{}, byChildrenResources...) + byDescendantsResources = append(byDescendantsResources, byScopeResources...) + byDescendantsResources = append(byDescendantsResources, byIdResources...) + + t.Run("simple pagination with destination id changes - descendants", func(t *testing.T) { + firstUpdatedA := byDescendantsResources[0] + // this no longer has the destination id that has permissions + firstUpdatedA.DestinationId = tar.GetPublicId() + firstUpdatedA, _, err := repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + // Descendants will keep permissions for everything so we don't elide + // one here, but we need to increment the version number to match + byDescendantsResources[0] = firstUpdatedA + t.Cleanup(func() { + firstUpdatedA.DestinationId = tar3.GetPublicId() + firstUpdatedA, _, err = repo.UpdateAlias(ctx, firstUpdatedA, firstUpdatedA.GetVersion(), []string{"DestinationId"}) + require.NoError(t, err) + byDescendantsResources[0] = firstUpdatedA + }) + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, byDescendantsPerms) + require.NoError(t, err) + require.NotNil(t, resp.ListToken) + require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp.CompleteListing) + require.Equal(t, resp.EstimatedItemCount, 15) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byDescendantsResources[0], cmpIgnoreUnexportedOpts)) + + // request remaining results -- we should see all, because descendants + // is going to maintain permissions + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 14, resp.ListToken, repo, byDescendantsPerms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp2.CompleteListing) + require.Equal(t, resp2.EstimatedItemCount, 15) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 14) + require.Empty(t, cmp.Diff(resp2.Items, byDescendantsResources[1:], cmpIgnoreUnexportedOpts)) + }) + + t.Run("simple pagination with deletion - id", func(t *testing.T) { deletedAliasId := byIdResources[0].GetPublicId() _, err := repo.DeleteAlias(ctx, deletedAliasId) require.NoError(t, err) @@ -499,7 +643,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NotNil(t, resp.ListToken) require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) require.False(t, resp.CompleteListing) - require.Equal(t, resp.EstimatedItemCount, 9) + require.Equal(t, 14, resp.EstimatedItemCount) require.Empty(t, resp.DeletedIds) require.Len(t, resp.Items, 1) require.Empty(t, cmp.Diff(resp.Items[0], byIdResources[0], cmpIgnoreUnexportedOpts)) @@ -509,7 +653,7 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp2.CompleteListing) - require.Equal(t, resp2.EstimatedItemCount, 9) + require.Equal(t, 14, resp2.EstimatedItemCount) require.Empty(t, resp2.DeletedIds) require.Len(t, resp2.Items, 3) require.Empty(t, cmp.Diff(resp2.Items, byIdResources[1:], cmpIgnoreUnexportedOpts)) @@ -528,7 +672,111 @@ func TestService_ListResolvableAliases(t *testing.T) { require.NoError(t, err) require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) require.True(t, resp3.CompleteListing) - require.Equal(t, resp3.EstimatedItemCount, 8) + require.Equal(t, 13, resp3.EstimatedItemCount) + require.Contains(t, resp3.DeletedIds, deletedAliasId) + require.Empty(t, resp3.Items) + }) + + t.Run("simple pagination with deletion - children", func(t *testing.T) { + deletedAliasId := byChildrenResources[0].GetPublicId() + _, err := repo.DeleteAlias(ctx, deletedAliasId) + require.NoError(t, err) + byChildrenResources = byChildrenResources[1:] + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, byChildrenPerms) + require.NoError(t, err) + require.NotNil(t, resp.ListToken) + require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp.CompleteListing) + require.Equal(t, 12, resp.EstimatedItemCount) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byChildrenResources[0], cmpIgnoreUnexportedOpts)) + + // request remaining results + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 8, resp.ListToken, repo, byChildrenPerms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp2.CompleteListing) + require.Equal(t, 12, resp2.EstimatedItemCount) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 3) + require.Empty(t, cmp.Diff(resp2.Items, byChildrenResources[1:], cmpIgnoreUnexportedOpts)) + + deletedAliasId = byChildrenResources[0].GetPublicId() + _, err = repo.DeleteAlias(ctx, deletedAliasId) + require.NoError(t, err) + byChildrenResources = byChildrenResources[1:] + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // request a refresh, nothing should be returned except the deleted id + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp2.ListToken, repo, byChildrenPerms) + require.NoError(t, err) + require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp3.CompleteListing) + require.Equal(t, 11, resp3.EstimatedItemCount) + require.Contains(t, resp3.DeletedIds, deletedAliasId) + require.Empty(t, resp3.Items) + }) + + // We have to re-build the expected set of resources as the original slices + // have been updated with updated values again + byDescendantsResources = append([]*target.Alias{}, byChildrenResources...) + byDescendantsResources = append(byDescendantsResources, byScopeResources...) + byDescendantsResources = append(byDescendantsResources, byIdResources...) + + t.Run("simple pagination with deletion - descendants", func(t *testing.T) { + deletedAliasId := byDescendantsResources[0].GetPublicId() + _, err := repo.DeleteAlias(ctx, deletedAliasId) + require.NoError(t, err) + byDescendantsResources = byDescendantsResources[1:] + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + resp, err := target.ListResolvableAliases(ctx, []byte("some hash"), 1, repo, byDescendantsPerms) + require.NoError(t, err) + require.NotNil(t, resp.ListToken) + require.Equal(t, resp.ListToken.GrantsHash, []byte("some hash")) + require.False(t, resp.CompleteListing) + require.Equal(t, 10, resp.EstimatedItemCount) + require.Empty(t, resp.DeletedIds) + require.Len(t, resp.Items, 1) + require.Empty(t, cmp.Diff(resp.Items[0], byDescendantsResources[0], cmpIgnoreUnexportedOpts)) + + // request remaining results + resp2, err := target.ListResolvableAliasesPage(ctx, []byte("some hash"), 13, resp.ListToken, repo, byDescendantsPerms) + require.NoError(t, err) + require.Equal(t, resp2.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp2.CompleteListing) + require.Equal(t, 10, resp2.EstimatedItemCount) + require.Empty(t, resp2.DeletedIds) + require.Len(t, resp2.Items, 9) + require.Empty(t, cmp.Diff(resp2.Items, byDescendantsResources[1:], cmpIgnoreUnexportedOpts)) + + deletedAliasId = byDescendantsResources[0].GetPublicId() + _, err = repo.DeleteAlias(ctx, deletedAliasId) + require.NoError(t, err) + byDescendantsResources = byDescendantsResources[1:] + + // Run analyze to update count estimate + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // request a refresh, nothing should be returned except the deleted id + resp3, err := target.ListResolvableAliasesRefresh(ctx, []byte("some hash"), 1, resp2.ListToken, repo, byDescendantsPerms) + require.NoError(t, err) + require.Equal(t, resp3.ListToken.GrantsHash, []byte("some hash")) + require.True(t, resp3.CompleteListing) + require.Equal(t, 9, resp3.EstimatedItemCount) require.Contains(t, resp3.DeletedIds, deletedAliasId) require.Empty(t, resp3.Items) }) diff --git a/internal/cmd/commands/rolescmd/funcs.go b/internal/cmd/commands/rolescmd/funcs.go index a89f2d4495..72b1d58076 100644 --- a/internal/cmd/commands/rolescmd/funcs.go +++ b/internal/cmd/commands/rolescmd/funcs.go @@ -259,7 +259,7 @@ func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]roles.Opti if len(c.flagGrants) > 0 { for _, grant := range c.flagGrants { - parsed, err := perms.Parse(c.Context, scope.Global.String(), grant) + parsed, err := perms.Parse(c.Context, perms.GrantTuple{RoleScopeId: scope.Global.String(), GrantScopeId: scope.Global.String(), Grant: grant}) if err != nil { c.UI.Error(fmt.Errorf("Grant %q could not be parsed successfully: %w", grant, err).Error()) return false diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 869f83564b..aaf671492f 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -267,6 +267,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { Id: opts.withId, Pin: opts.withPin, Type: opts.withType, + // Parent Scope ID will be filled in via performAuthCheck } // Global scope has no parent ID; account for this if opts.withId == scope.Global.String() && opts.withType == resource.Scope { @@ -330,7 +331,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { grants = append(grants, event.Grant{ Grant: g.Grant, RoleId: g.RoleId, - ScopeId: g.ScopeId, + ScopeId: g.GrantScopeId, }) } ea.UserInfo = &event.UserInfo{ @@ -630,6 +631,7 @@ func (v verifier) performAuthCheck(ctx context.Context) ( ParentScopeId: scp.GetParentId(), } } + v.res.ParentScopeId = scopeInfo.ParentScopeId // At this point we don't need to look up grants since it's automatically allowed if v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms) { @@ -652,7 +654,7 @@ func (v verifier) performAuthCheck(ctx context.Context) ( // Note: Below, we always skip validation so that we don't error on formats // that we've since restricted, e.g. "ids=foo;actions=create,read". These // will simply not have an effect. - for _, pair := range grantTuples { + for _, tuple := range grantTuples { permsOpts := []perms.Option{ perms.WithUserId(*userData.User.Id), perms.WithSkipFinalValidation(true), @@ -662,11 +664,10 @@ func (v verifier) performAuthCheck(ctx context.Context) ( } parsed, err := perms.Parse( ctx, - pair.ScopeId, - pair.Grant, + tuple, permsOpts...) if err != nil { - retErr = errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to parse grant %#v", pair.Grant))) + retErr = errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to parse grant %#v", tuple.Grant))) return } parsedGrants = append(parsedGrants, parsed) @@ -861,7 +862,7 @@ func (r *VerifyResults) ScopesAuthorizedForList(ctx context.Context, rootScopeId aSet := r.FetchActionSetForType(ctx, resource.Unknown, // This is overridden by `WithResource` option. action.NewActionSet(action.List), - WithResource(&perms.Resource{Type: resourceType, ScopeId: scpId}), + WithResource(&perms.Resource{Type: resourceType, ScopeId: scpId, ParentScopeId: scp.GetParentId()}), ) // We only expect the action set to be nothing, or list. In case diff --git a/internal/daemon/controller/auth/authorized_actions.go b/internal/daemon/controller/auth/authorized_actions.go index 3e5cd96d3c..07a1dd95f5 100644 --- a/internal/daemon/controller/auth/authorized_actions.go +++ b/internal/daemon/controller/auth/authorized_actions.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" "github.com/hashicorp/go-secure-stdlib/strutil" "google.golang.org/protobuf/types/known/structpb" ) @@ -22,11 +23,12 @@ import ( func CalculateAuthorizedCollectionActions(ctx context.Context, authResults VerifyResults, mapToRange map[resource.Type]action.ActionSet, - scopeId, pin string, + scopeInfo *scopes.ScopeInfo, pin string, ) (map[string]*structpb.ListValue, error) { res := &perms.Resource{ - ScopeId: scopeId, - Pin: pin, + ScopeId: scopeInfo.GetId(), + Pin: pin, + ParentScopeId: scopeInfo.GetParentScopeId(), } // Range over the defined collections and check permissions against those // collections. diff --git a/internal/daemon/controller/handlers/accounts/account_service_test.go b/internal/daemon/controller/handlers/accounts/account_service_test.go index a1498a542a..5e33ea3741 100644 --- a/internal/daemon/controller/handlers/accounts/account_service_test.go +++ b/internal/daemon/controller/handlers/accounts/account_service_test.go @@ -4385,3 +4385,118 @@ func TestChangePassword(t *testing.T) { }) } } + +// The purpose of this test is mainly to ensure that we are properly fetching +// membership information in GrantsForUser across managed group types +func TestGrantsAcrossManagedGroups(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcAm := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + oidcAcct := oidc.TestAccount(t, conn, oidcAm, "test-subject") + // Create a managed group that will always match, so we can test that it is + // returned in results + oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, `"/token/sub" matches ".*"`) + oidc.TestManagedGroupMember(t, conn, oidcMg.GetPublicId(), oidcAcct.GetPublicId()) + + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + ldapAcct := ldap.TestAccount(t, conn, ldapAm, "test-acct", + ldap.WithMemberOfGroups(ctx, "admin"), + ldap.WithFullName(ctx, "test-name"), + ldap.WithEmail(ctx, "test-email"), + ldap.WithDn(ctx, "test-dn"), + ) + ldapMg := ldap.TestManagedGroup(t, conn, ldapAm, []string{"admin"}) + + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepo, err := iamRepoFn() + require.NoError(t, err) + + user := iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(oidcAcct.GetPublicId(), ldapAcct.GetPublicId())) + + // Create two roles, each containing a single managed group, and add a + // unique grant to each + oidcRole := iam.TestRole(t, conn, org.GetPublicId(), iam.WithGrantScopeIds([]string{globals.GrantScopeChildren})) + iam.TestManagedGroupRole(t, conn, oidcRole.GetPublicId(), oidcMg.GetPublicId()) + iam.TestRoleGrant(t, conn, oidcRole.GetPublicId(), "ids=ttcp_oidc;actions=read") + ldapRole := iam.TestRole(t, conn, org.GetPublicId(), iam.WithGrantScopeIds([]string{globals.GrantScopeChildren})) + iam.TestManagedGroupRole(t, conn, ldapRole.GetPublicId(), ldapMg.GetPublicId()) + iam.TestRoleGrant(t, conn, ldapRole.GetPublicId(), "ids=ttcp_ldap;actions=read") + + grants, err := iamRepo.GrantsForUser(ctx, user.GetPublicId()) + require.NoError(t, err) + + // Verify we see both grants + var foundOidc, foundLdap bool + for _, grant := range grants { + if grant.Grant == "ids=ttcp_oidc;actions=read" { + foundOidc = true + } + if grant.Grant == "ids=ttcp_ldap;actions=read" { + foundLdap = true + } + } + assert.True(t, foundOidc) + assert.True(t, foundLdap) + + // Delete the ldap managed group + ldapRepo, err := ldap.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + numDeleted, err := ldapRepo.DeleteManagedGroup(ctx, org.GetPublicId(), ldapMg.GetPublicId()) + require.NoError(t, err) + assert.Equal(t, 1, numDeleted) + + // Verify we don't see the ldap grant anymore + grants, err = iamRepo.GrantsForUser(ctx, user.GetPublicId()) + require.NoError(t, err) + foundOidc = false + foundLdap = false + for _, grant := range grants { + if grant.Grant == "ids=ttcp_oidc;actions=read" { + foundOidc = true + } + if grant.Grant == "ids=ttcp_ldap;actions=read" { + foundLdap = true + } + } + assert.True(t, foundOidc) + assert.False(t, foundLdap) + + // Delete the oidc managed group + oidcRepo, err := oidc.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + numDeleted, err = oidcRepo.DeleteManagedGroup(ctx, org.GetPublicId(), oidcMg.GetPublicId()) + require.NoError(t, err) + assert.Equal(t, 1, numDeleted) + + // Verify we don't see the oidc grant anymore + grants, err = iamRepo.GrantsForUser(ctx, user.GetPublicId()) + require.NoError(t, err) + foundOidc = false + foundLdap = false + for _, grant := range grants { + if grant.Grant == "ids=ttcp_oidc;actions=read" { + foundOidc = true + } + if grant.Grant == "ids=ttcp_ldap;actions=read" { + foundLdap = true + } + } + assert.False(t, foundOidc) + assert.False(t, foundLdap) +} diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service.go b/internal/daemon/controller/handlers/authmethods/authmethod_service.go index f923c55341..b286f14a82 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service.go @@ -343,7 +343,7 @@ func (s Service) GetAuthMethod(ctx context.Context, req *pbs.GetAuthMethodReques outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(am.GetPublicId()).Subtype]).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope, am.GetPublicId()) if err != nil { return nil, err } @@ -388,7 +388,7 @@ func (s Service) CreateAuthMethod(ctx context.Context, req *pbs.CreateAuthMethod outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(am.GetPublicId()).Subtype]).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope, am.GetPublicId()) if err != nil { return nil, err } @@ -438,7 +438,7 @@ func (s Service) UpdateAuthMethod(ctx context.Context, req *pbs.UpdateAuthMethod outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(am.GetPublicId()).Subtype]).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope, am.GetPublicId()) if err != nil { return nil, err } @@ -492,7 +492,7 @@ func (s Service) ChangeState(ctx context.Context, req *pbs.ChangeStateRequest) ( outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(am.GetPublicId()).Subtype]).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope, am.GetPublicId()) if err != nil { return nil, err } @@ -1441,6 +1441,10 @@ func (s Service) convertToAuthenticateResponse(ctx context.Context, req *pbs.Aut ScopeId: authResults.Scope.Id, Type: resource.AuthToken, } + // Auth methods are only at global or org, so we can figure out the parent + if strings.HasPrefix(res.ScopeId, scope.Org.Prefix()) { + res.ParentScopeId = scope.Global.String() + } tokenType := req.GetType() if tokenType == "" { // Fall back to deprecated field if type is not set @@ -1587,7 +1591,7 @@ func newOutputOpts(ctx context.Context, item auth.AuthMethod, scopeInfoMap map[s outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, item.GetPublicId()) + collectionActions, err := requestauth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope, item.GetPublicId()) if err != nil { return nil, false, err } diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go index ae3f881d7d..694f2bd588 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go @@ -996,10 +996,10 @@ func calculateAuthorizedCollectionActions(ctx context.Context, authResults auth. var err error switch globals.ResourceInfoFromPrefix(id).Subtype { case vault.Subtype: - collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, vaultCollectionTypeMap, authResults.Scope.Id, id) + collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, vaultCollectionTypeMap, authResults.Scope, id) case static.Subtype: - collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, staticCollectionTypeMap, authResults.Scope.Id, id) + collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, staticCollectionTypeMap, authResults.Scope, id) } if err != nil { return nil, err diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go index 1e96304983..26b0f7b3fd 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go @@ -342,7 +342,7 @@ func (s Service) GetHostCatalog(ctx context.Context, req *pbs.GetHostCatalogRequ subtype = hostplugin.Subtype } if subtype != "" { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope.Id, hc.GetPublicId()) + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope, hc.GetPublicId()) if err != nil { return nil, err } @@ -399,7 +399,7 @@ func (s Service) CreateHostCatalog(ctx context.Context, req *pbs.CreateHostCatal subtype = hostplugin.Subtype } if subtype != "" { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope.Id, hc.GetPublicId()) + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope, hc.GetPublicId()) if err != nil { return nil, err } @@ -462,7 +462,7 @@ func (s Service) UpdateHostCatalog(ctx context.Context, req *pbs.UpdateHostCatal subtype = hostplugin.Subtype } if subtype != "" { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope.Id, hc.GetPublicId()) + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope, hc.GetPublicId()) if err != nil { return nil, err } @@ -795,7 +795,7 @@ func newOutputOpts( subtype = hostplugin.Subtype } if subtype != "" { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope.Id, item.GetPublicId()) + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap[subtype], authResults.Scope, item.GetPublicId()) if err != nil { return nil, false, err } diff --git a/internal/daemon/controller/handlers/roles/role_service.go b/internal/daemon/controller/handlers/roles/role_service.go index 091777296b..81c743bcf5 100644 --- a/internal/daemon/controller/handlers/roles/role_service.go +++ b/internal/daemon/controller/handlers/roles/role_service.go @@ -1123,7 +1123,7 @@ func toProto(ctx context.Context, in *iam.Role, principals []*iam.PrincipalRole, } if outputFields.Has(globals.GrantsField) { for _, g := range grants { - parsed, err := perms.Parse(ctx, in.GetScopeId(), g.GetRawGrant()) + parsed, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: in.GetPublicId(), GrantScopeId: in.GetScopeId(), Grant: g.GetRawGrant()}) if err != nil { // This should never happen as we validate on the way in, but let's // return what we can since we are still returning the raw grant @@ -1319,7 +1319,7 @@ func validateAddRoleGrantsRequest(ctx context.Context, req *pbs.AddRoleGrantsReq badFields["grant_strings"] = "Grant strings must not be empty." break } - grant, err := perms.Parse(ctx, "p_anything", v) + grant, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: req.GetId(), GrantScopeId: "p_anything", Grant: v}) if err != nil { badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q.", v) break @@ -1356,9 +1356,9 @@ func validateSetRoleGrantsRequest(ctx context.Context, req *pbs.SetRoleGrantsReq badFields["grant_strings"] = "Grant strings must not be empty." break } - grant, err := perms.Parse(ctx, "p_anything", v) + grant, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: req.GetId(), GrantScopeId: "p_anything", Grant: v}) if err != nil { - badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q.", v) + badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q: %s.", v, err.Error()) break } _, actStrs := grant.Actions() @@ -1396,7 +1396,7 @@ func validateRemoveRoleGrantsRequest(ctx context.Context, req *pbs.RemoveRoleGra badFields["grant_strings"] = "Grant strings must not be empty." break } - if _, err := perms.Parse(ctx, "p_anything", v); err != nil { + if _, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: req.GetId(), GrantScopeId: "p_anything", Grant: v}); err != nil { badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q.", v) break } diff --git a/internal/daemon/controller/handlers/roles/role_service_test.go b/internal/daemon/controller/handlers/roles/role_service_test.go index 05524d693f..17c0535d52 100644 --- a/internal/daemon/controller/handlers/roles/role_service_test.go +++ b/internal/daemon/controller/handlers/roles/role_service_test.go @@ -1026,7 +1026,7 @@ func TestCreate(t *testing.T) { func TestUpdate(t *testing.T) { ctx := context.Background() grantString := "ids=*;type=*;actions=*" - g, err := perms.Parse(context.Background(), "global", grantString) + g, err := perms.Parse(context.Background(), perms.GrantTuple{RoleScopeId: "global", GrantScopeId: "global", Grant: grantString}) require.NoError(t, err) _, actions := g.Actions() grant := &pb.Grant{ @@ -2127,7 +2127,7 @@ func checkEqualGrants(t *testing.T, expected []string, got *pb.Role) { return got.GrantStrings[i] < got.GrantStrings[j] }) for i, v := range expected { - parsed, err := perms.Parse(context.Background(), "o_abc123", v) + parsed, err := perms.Parse(context.Background(), perms.GrantTuple{RoleScopeId: "o_abc123", GrantScopeId: "o_abc123", Grant: v}) require.NoError(err) assert.Equal(expected[i], got.GrantStrings[i]) assert.Equal(expected[i], got.Grants[i].GetRaw()) diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index 17cdf0ad2c..951015de39 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -337,7 +337,14 @@ func (s *Service) GetScope(ctx context.Context, req *pbs.GetScopeRequest) (*pbs. outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), idActionsById(p.GetPublicId())).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], p.GetPublicId(), "") + scopeInfo := &pb.ScopeInfo{ + Id: p.GetPublicId(), + Type: p.Type, + Name: p.GetName(), + Description: p.GetDescription(), + ParentScopeId: p.GetParentId(), + } + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], scopeInfo, "") if err != nil { return nil, err } @@ -382,7 +389,14 @@ func (s *Service) CreateScope(ctx context.Context, req *pbs.CreateScopeRequest) outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), idActionsById(p.GetPublicId())).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], p.GetPublicId(), "") + scopeInfo := &pb.ScopeInfo{ + Id: p.GetPublicId(), + Type: p.Type, + Name: p.GetName(), + Description: p.GetDescription(), + ParentScopeId: p.GetParentId(), + } + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], scopeInfo, "") if err != nil { return nil, err } @@ -427,7 +441,14 @@ func (s *Service) UpdateScope(ctx context.Context, req *pbs.UpdateScopeRequest) outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), idActionsById(p.GetPublicId())).Strings())) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], p.GetPublicId(), "") + scopeInfo := &pb.ScopeInfo{ + Id: p.GetPublicId(), + Type: p.Type, + Name: p.GetName(), + Description: p.GetDescription(), + ParentScopeId: p.GetParentId(), + } + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], scopeInfo, "") if err != nil { return nil, err } @@ -1137,7 +1158,14 @@ func newOutputOpts(ctx context.Context, item *iam.Scope, authResults auth.Verify outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) } if outputFields.Has(globals.AuthorizedCollectionActionsField) { - collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[item.Type], item.GetPublicId(), "") + scopeInfo := &pb.ScopeInfo{ + Id: item.GetPublicId(), + Type: item.Type, + Name: item.GetName(), + Description: item.GetDescription(), + ParentScopeId: item.GetParentId(), + } + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[item.Type], scopeInfo, "") if err != nil { return nil, false, err } diff --git a/internal/daemon/controller/handlers/sessions/session_service.go b/internal/daemon/controller/handlers/sessions/session_service.go index 7df9fed100..59230bb0a1 100644 --- a/internal/daemon/controller/handlers/sessions/session_service.go +++ b/internal/daemon/controller/handlers/sessions/session_service.go @@ -575,9 +575,10 @@ func validateCancelRequest(req *pbs.CancelSessionRequest) error { func newOutputOpts(ctx context.Context, item *session.Session, scopeIds map[string]*scopes.ScopeInfo, authResults auth.VerifyResults) ([]handlers.Option, bool) { res := perms.Resource{ - Type: resource.Session, - Id: item.GetPublicId(), - ScopeId: item.GetProjectId(), + Type: resource.Session, + Id: item.GetPublicId(), + ScopeId: item.GetProjectId(), + ParentScopeId: scopeIds[item.ProjectId].ParentScopeId, } authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() if len(authorizedActions) == 0 { diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 48e5be7358..e05eeaec49 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -2006,7 +2006,7 @@ func validateListRequest(ctx context.Context, req *pbs.ListTargetsRequest) error } func newOutputOpts(ctx context.Context, item target.Target, authResults auth.VerifyResults, authzScopes map[string]*scopes.ScopeInfo) []handlers.Option { - pr := perms.Resource{Id: item.GetPublicId(), ScopeId: item.GetProjectId(), Type: resource.Target} + pr := perms.Resource{Id: item.GetPublicId(), ScopeId: item.GetProjectId(), Type: resource.Target, ParentScopeId: authzScopes[item.GetProjectId()].GetParentScopeId()} outputFields := authResults.FetchOutputFields(pr, action.List).SelfOrDefaults(authResults.UserId) outputOpts := make([]handlers.Option, 0, 3) diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index bc9db3588b..92274e83e1 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -489,13 +489,8 @@ func TestList(t *testing.T) { } } -func TestListPagination(t *testing.T) { - // Set database read timeout to avoid duplicates in response - oldReadTimeout := globals.RefreshReadLookbackDuration - globals.RefreshReadLookbackDuration = 0 - t.Cleanup(func() { - globals.RefreshReadLookbackDuration = oldReadTimeout - }) +func TestListGrantScopes(t *testing.T) { + t.Parallel() ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") sqlDB, err := conn.SqlDB(ctx) @@ -515,315 +510,628 @@ func TestListPagination(t *testing.T) { serversRepoFn := func() (*server.Repository, error) { return server.NewRepository(ctx, rw, rw, kms) } - repo, err := target.NewRepository(ctx, rw, rw, kms) - require.NoError(t, err) - org, proj := iam.TestScopes(t, iamRepo) - at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) - r := iam.TestRole(t, conn, proj.GetPublicId()) - _ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId()) - _ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*") - hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] - hss := static.TestSets(t, conn, hc.GetPublicId(), 2) - s, err := testService(t, context.Background(), conn, kms, wrapper) - require.NoError(t, err) + at := authtoken.TestAuthToken(t, conn, kms, scope.Global.String()) - var allTargets []*pb.Target - for i := 0; i < 10; i++ { - tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), fmt.Sprintf("tar%d", i), target.WithHostSources([]string{hss[0].GetPublicId(), hss[1].GetPublicId()})) - allTargets = append(allTargets, &pb.Target{ - Id: tar.GetPublicId(), - ScopeId: proj.GetPublicId(), - Name: wrapperspb.String(tar.GetName()), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, - CreatedTime: tar.GetCreateTime().GetTimestamp(), - UpdatedTime: tar.GetUpdateTime().GetTimestamp(), - Version: tar.GetVersion(), - Type: tcp.Subtype.String(), - Attrs: &pb.Target_TcpTargetAttributes{}, - SessionMaxSeconds: wrapperspb.UInt32(28800), - SessionConnectionLimit: wrapperspb.Int32(-1), - AuthorizedActions: testAuthorizedActions, - Address: &wrapperspb.StringValue{}, - }) + var projects []*iam.Scope + org1, proj1 := iam.TestScopes(t, iamRepo) + projects = append(projects, proj1) + org2, proj2 := iam.TestScopes(t, iamRepo) + projects = append(projects, proj2) + + var totalTars []*pb.Target + for i, proj := range projects { + for j := 0; j < 5; j++ { + name := fmt.Sprintf("tar-%d-%d", i, j) + tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), name, target.WithAddress(fmt.Sprintf("1.1.%d.%d", i, j))) + totalTars = append(totalTars, &pb.Target{ + Id: tar.GetPublicId(), + ScopeId: proj.GetPublicId(), + Name: wrapperspb.String(name), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: proj.ParentId}, + CreatedTime: tar.GetCreateTime().GetTimestamp(), + UpdatedTime: tar.GetUpdateTime().GetTimestamp(), + Version: tar.GetVersion(), + Type: tcp.Subtype.String(), + Attrs: &pb.Target_TcpTargetAttributes{}, + SessionMaxSeconds: wrapperspb.UInt32(28800), + SessionConnectionLimit: wrapperspb.Int32(-1), + AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{Value: fmt.Sprintf("1.1.%d.%d", i, j)}, + }) + } } - // Reverse since we read items in descending order (newest first) - slices.Reverse(allTargets) // Run analyze to update postgres estimates _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(t, err) - requestInfo := authpb.RequestInfo{ - TokenFormat: uint32(auth.AuthTokenTypeBearer), - PublicId: at.GetPublicId(), - Token: at.GetToken(), - } - requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) - ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + _ = org1 + _ = org2 - // Start paginating, recursively - req := &pbs.ListTargetsRequest{ - ScopeId: "global", - Recursive: true, - Filter: "", - ListToken: "", - PageSize: 2, - } - got, err := s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 2) - // Compare without comparing the list token - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: allTargets[0:2], - ResponseType: "delta", + cases := []struct { + name string + pageSize uint32 + setupFunc func(t *testing.T) + res *pbs.ListTargetsResponse + err error + }{ + { + name: "global-with-direct-grants-wildcard", + setupFunc: func(t *testing.T) { + globalRole := iam.TestRole(t, conn, scope.Global.String(), iam.WithGrantScopeIds([]string{proj1.GetPublicId(), proj2.GetPublicId()})) + _ = iam.TestUserRole(t, conn, globalRole.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, globalRole.GetPublicId(), "ids=*;type=*;actions=*") + }, + res: &pbs.ListTargetsResponse{ + Items: totalTars, + ResponseType: "complete", SortBy: "created_time", SortDir: "desc", - RemovedIds: nil, EstItemCount: 10, }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - - // Request second page - req.ListToken = got.ListToken - got, err = s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 2) - // Compare without comparing the list token - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: allTargets[2:4], - ResponseType: "delta", + }, + { + name: "global-with-direct-grants-non-wildcard", + setupFunc: func(t *testing.T) { + globalRole := iam.TestRole(t, conn, scope.Global.String(), iam.WithGrantScopeIds([]string{proj1.GetPublicId(), proj2.GetPublicId()})) + _ = iam.TestUserRole(t, conn, globalRole.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, globalRole.GetPublicId(), "ids=*;type=target;actions=list") + _ = iam.TestRoleGrant(t, conn, globalRole.GetPublicId(), fmt.Sprintf("ids=%s,%s;actions=*", totalTars[0].Id, totalTars[1].Id)) + }, + res: &pbs.ListTargetsResponse{ + Items: totalTars[0:2], + ResponseType: "complete", SortBy: "created_time", SortDir: "desc", - RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 2, }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - - // Request rest of results - req.ListToken = got.ListToken - req.PageSize = 10 - got, err = s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 6) - // Compare without comparing the list token - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: allTargets[4:], + }, + { + name: "global-with-descendants-wildcard", + setupFunc: func(t *testing.T) { + globalRole := iam.TestRole(t, conn, scope.Global.String(), iam.WithGrantScopeIds([]string{globals.GrantScopeDescendants})) + _ = iam.TestUserRole(t, conn, globalRole.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, globalRole.GetPublicId(), "ids=*;type=*;actions=*") + }, + res: &pbs.ListTargetsResponse{ + Items: totalTars, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", - RemovedIds: nil, EstItemCount: 10, }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - - // Create another target - tar := tcp.TestTarget(ctx, t, conn, proj.GetPublicId(), "test-target", target.WithHostSources([]string{hss[0].GetPublicId(), hss[1].GetPublicId()})) - newTarget := &pb.Target{ - Id: tar.GetPublicId(), - ScopeId: proj.GetPublicId(), - Name: wrapperspb.String(tar.GetName()), - Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, - CreatedTime: tar.GetCreateTime().GetTimestamp(), - UpdatedTime: tar.GetUpdateTime().GetTimestamp(), - Version: tar.GetVersion(), - Type: tcp.Subtype.String(), - Attrs: &pb.Target_TcpTargetAttributes{}, - SessionMaxSeconds: wrapperspb.UInt32(28800), - SessionConnectionLimit: wrapperspb.Int32(-1), - AuthorizedActions: testAuthorizedActions, - Address: &wrapperspb.StringValue{}, - } - // Add to the front since it's most recently updated - allTargets = append([]*pb.Target{newTarget}, allTargets...) - - // Delete one of the other targets - _, err = repo.DeleteTarget(ctx, allTargets[len(allTargets)-1].Id) - require.NoError(t, err) - deletedTarget := allTargets[len(allTargets)-1] - allTargets = allTargets[:len(allTargets)-1] - - // Update one of the other targets - allTargets[1].Name = wrapperspb.String("new-name") - allTargets[1].Version = 2 - updatedTarget := &tcp.Target{ - Target: &store.Target{ - PublicId: allTargets[1].Id, - Name: allTargets[1].Name.GetValue(), - ProjectId: allTargets[1].ScopeId, }, - } - tg, _, err := repo.UpdateTarget(ctx, updatedTarget, 1, []string{"name"}) - require.NoError(t, err) - allTargets[1].UpdatedTime = tg.GetUpdateTime().GetTimestamp() - allTargets[1].Version = tg.GetVersion() - // Add to the front since it's most recently updated - allTargets = append( - []*pb.Target{allTargets[1]}, - append( - []*pb.Target{allTargets[0]}, - allTargets[2:]..., - )..., - ) - - // Run analyze to update postgres estimates - _, err = sqlDB.ExecContext(ctx, "analyze") - require.NoError(t, err) - - // Request updated results - req.ListToken = got.ListToken - req.PageSize = 1 - got, err = s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 1) - // Compare without comparing the list token - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: []*pb.Target{allTargets[0]}, - ResponseType: "delta", - SortBy: "updated_time", - SortDir: "desc", - // Should contain the deleted target - RemovedIds: []string{deletedTarget.Id}, - EstItemCount: 10, + { + name: "org-with-direct-grants-wildcard", + setupFunc: func(t *testing.T) { + org1Role := iam.TestRole(t, conn, org1.GetPublicId(), iam.WithGrantScopeIds([]string{proj1.GetPublicId()})) + _ = iam.TestUserRole(t, conn, org1Role.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, org1Role.GetPublicId(), "ids=*;type=*;actions=*") + org2Role := iam.TestRole(t, conn, org2.GetPublicId(), iam.WithGrantScopeIds([]string{proj2.GetPublicId()})) + _ = iam.TestUserRole(t, conn, org2Role.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, org2Role.GetPublicId(), "ids=*;type=*;actions=*") }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - - // Get next page - req.ListToken = got.ListToken - got, err = s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 1) - // Compare without comparing the list token - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: []*pb.Target{allTargets[1]}, + res: &pbs.ListTargetsResponse{ + Items: totalTars, ResponseType: "complete", - SortBy: "updated_time", + SortBy: "created_time", SortDir: "desc", - RemovedIds: nil, EstItemCount: 10, }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - - // Request new page with filter requiring looping - // to fill the page. - req.ListToken = "" - req.PageSize = 1 - req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, allTargets[len(allTargets)-2].Id, allTargets[len(allTargets)-1].Id) - got, err = s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 1) - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: []*pb.Target{allTargets[len(allTargets)-2]}, - ResponseType: "delta", + }, + { + name: "org-with-direct-grants-non-wildcard", + setupFunc: func(t *testing.T) { + org1Role := iam.TestRole(t, conn, org1.GetPublicId(), iam.WithGrantScopeIds([]string{proj1.GetPublicId()})) + _ = iam.TestUserRole(t, conn, org1Role.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, org1Role.GetPublicId(), "ids=*;type=target;actions=list") + _ = iam.TestRoleGrant(t, conn, org1Role.GetPublicId(), fmt.Sprintf("ids=%s,%s;actions=*", totalTars[0].Id, totalTars[1].Id)) + org2Role := iam.TestRole(t, conn, org2.GetPublicId(), iam.WithGrantScopeIds([]string{proj2.GetPublicId()})) + _ = iam.TestUserRole(t, conn, org2Role.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, org2Role.GetPublicId(), "ids=*;type=target;actions=list") + _ = iam.TestRoleGrant(t, conn, org2Role.GetPublicId(), fmt.Sprintf("ids=%s,%s;actions=*", totalTars[5].Id, totalTars[6].Id)) + }, + res: &pbs.ListTargetsResponse{ + Items: append([]*pb.Target{}, append(append([]*pb.Target{}, totalTars[0:2]...), totalTars[5:7]...)...), + ResponseType: "complete", SortBy: "created_time", SortDir: "desc", - // Should be empty again - RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 4, }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - req.ListToken = got.ListToken - // Get the second page - got, err = s.ListTargets(ctx, req) - require.NoError(t, err) - require.Len(t, got.GetItems(), 1) - assert.Empty(t, - cmp.Diff( - got, - &pbs.ListTargetsResponse{ - Items: []*pb.Target{allTargets[len(allTargets)-1]}, + }, + { + name: "org-with-children-wildcard", + setupFunc: func(t *testing.T) { + org1Role := iam.TestRole(t, conn, org1.GetPublicId(), iam.WithGrantScopeIds([]string{globals.GrantScopeChildren})) + _ = iam.TestUserRole(t, conn, org1Role.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, org1Role.GetPublicId(), "ids=*;type=*;actions=*") + }, + res: &pbs.ListTargetsResponse{ + Items: totalTars[0:5], ResponseType: "complete", SortBy: "created_time", SortDir: "desc", - RemovedIds: nil, - EstItemCount: 10, + EstItemCount: 5, }, - cmpopts.SortSlices(func(a, b string) bool { - return a < b - }), - protocmp.Transform(), - protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), - ), - ) - - // Create unauthenticated user - unauthAt := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) - unauthR := iam.TestRole(t, conn, proj.GetPublicId()) - _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) - - // Make a request with the unauthenticated user, - // ensure the response contains the pagination parameters. - requestInfo = authpb.RequestInfo{ - TokenFormat: uint32(auth.AuthTokenTypeBearer), - PublicId: unauthAt.GetPublicId(), - Token: unauthAt.GetToken(), + }, } - requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) - ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, err := sqlDB.Exec("delete from iam_role") + require.NoError(err) + tc.setupFunc(t) + + s, err := testService(t, context.Background(), conn, kms, wrapper) + require.NoError(err, "Couldn't create new target service.") + + requestInfo := authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: at.GetPublicId(), + Token: at.GetToken(), + } + requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx := auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + got, gErr := s.ListTargets(ctx, &pbs.ListTargetsRequest{ + ScopeId: scope.Global.String(), + Recursive: true, + PageSize: tc.pageSize, + }) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "got error %v, wanted %v", gErr, tc.err) + return + } + require.NoError(gErr) + assert.Equal(len(tc.res.Items), len(got.Items)) + wantById := make(map[string]*pb.Target, len(tc.res.Items)) + for _, t := range tc.res.Items { + wantById[t.Id] = t + } + for _, t := range got.Items { + want, ok := wantById[t.Id] + assert.True(ok, "Got unexpected target with id: %s", t.Id) + assert.Empty(cmp.Diff( + t, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "got %v, wanted %v", t, want) + } + }) + } +} + +func TestListPagination(t *testing.T) { + testListPagination := func(t *testing.T, useDescendants bool) { + // Set database read timeout to avoid duplicates in response + oldReadTimeout := globals.RefreshReadLookbackDuration + globals.RefreshReadLookbackDuration = 0 + t.Cleanup(func() { + globals.RefreshReadLookbackDuration = oldReadTimeout + }) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + sqlDB, err := conn.SqlDB(ctx) + require.NoError(t, err) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + rw := db.New(conn) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + tokenRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ctx, rw, rw, kms) + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kms) + } + repo, err := target.NewRepository(ctx, rw, rw, kms) + require.NoError(t, err) + + // We're going to run the same test in two projects; one with + // descendants and one with direct grants in one project and a child + // grant from org in another project + org1, proj1 := iam.TestScopes(t, iamRepo) + org2, proj2 := iam.TestScopes(t, iamRepo) + at := authtoken.TestAuthToken(t, conn, kms, scope.Global.String()) + if useDescendants { + r := iam.TestRole(t, conn, scope.Global.String(), iam.WithGrantScopeIds([]string{globals.GrantScopeDescendants})) + _ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*") + } else { + r1 := iam.TestRole(t, conn, proj1.GetPublicId()) + _ = iam.TestUserRole(t, conn, r1.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, r1.GetPublicId(), "ids=*;type=*;actions=*") + r2 := iam.TestRole(t, conn, org2.GetPublicId(), iam.WithGrantScopeIds([]string{globals.GrantScopeChildren})) + _ = iam.TestUserRole(t, conn, r2.GetPublicId(), at.GetIamUserId()) + _ = iam.TestRoleGrant(t, conn, r2.GetPublicId(), "ids=*;type=*;actions=*") + } + hc := static.TestCatalogs(t, conn, proj1.GetPublicId(), 1)[0] + hss := static.TestSets(t, conn, hc.GetPublicId(), 2) + s, err := testService(t, context.Background(), conn, kms, wrapper) + require.NoError(t, err) + + var allTargets []*pb.Target + for i := 0; i < 10; i++ { + tar := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), fmt.Sprintf("tar-1-%d", i), target.WithHostSources([]string{hss[0].GetPublicId(), hss[1].GetPublicId()})) + allTargets = append(allTargets, &pb.Target{ + Id: tar.GetPublicId(), + ScopeId: proj1.GetPublicId(), + Name: wrapperspb.String(tar.GetName()), + Scope: &scopes.ScopeInfo{Id: proj1.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org1.GetPublicId()}, + CreatedTime: tar.GetCreateTime().GetTimestamp(), + UpdatedTime: tar.GetUpdateTime().GetTimestamp(), + Version: tar.GetVersion(), + Type: tcp.Subtype.String(), + Attrs: &pb.Target_TcpTargetAttributes{}, + SessionMaxSeconds: wrapperspb.UInt32(28800), + SessionConnectionLimit: wrapperspb.Int32(-1), + AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, + }) + } + for i := 0; i < 10; i++ { + tar := tcp.TestTarget(ctx, t, conn, proj2.GetPublicId(), fmt.Sprintf("tar-2-%d", i), target.WithAddress(fmt.Sprintf("127.0.0.%d", i))) + allTargets = append(allTargets, &pb.Target{ + Id: tar.GetPublicId(), + ScopeId: proj2.GetPublicId(), + Name: wrapperspb.String(tar.GetName()), + Scope: &scopes.ScopeInfo{Id: proj2.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org2.GetPublicId()}, + CreatedTime: tar.GetCreateTime().GetTimestamp(), + UpdatedTime: tar.GetUpdateTime().GetTimestamp(), + Version: tar.GetVersion(), + Type: tcp.Subtype.String(), + Attrs: &pb.Target_TcpTargetAttributes{}, + SessionMaxSeconds: wrapperspb.UInt32(28800), + SessionConnectionLimit: wrapperspb.Int32(-1), + AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{Value: fmt.Sprintf("127.0.0.%d", i)}, + }) + } + // Reverse since we read items in descending order (newest first) + slices.Reverse(allTargets) + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + requestInfo := authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: at.GetPublicId(), + Token: at.GetToken(), + } + requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + + // Start paginating, recursively + req := &pbs.ListTargetsRequest{ + ScopeId: "global", + Recursive: true, + Filter: "", + ListToken: "", + PageSize: 2, + } + got, err := s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: allTargets[0:2], + ResponseType: "delta", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) + + // Request second page + req.ListToken = got.ListToken + got, err = s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: allTargets[2:4], + ResponseType: "delta", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) + + // Request rest of results + req.ListToken = got.ListToken + req.PageSize = 20 + got, err = s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 16) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: allTargets[4:], + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) - got, err = s.ListTargets(ctx, &pbs.ListTargetsRequest{ - ScopeId: "global", - Recursive: true, + // Create another target + tar := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), "test-target-1", target.WithHostSources([]string{hss[0].GetPublicId(), hss[1].GetPublicId()})) + newTarget := &pb.Target{ + Id: tar.GetPublicId(), + ScopeId: proj1.GetPublicId(), + Name: wrapperspb.String(tar.GetName()), + Scope: &scopes.ScopeInfo{Id: proj1.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org1.GetPublicId()}, + CreatedTime: tar.GetCreateTime().GetTimestamp(), + UpdatedTime: tar.GetUpdateTime().GetTimestamp(), + Version: tar.GetVersion(), + Type: tcp.Subtype.String(), + Attrs: &pb.Target_TcpTargetAttributes{}, + SessionMaxSeconds: wrapperspb.UInt32(28800), + SessionConnectionLimit: wrapperspb.Int32(-1), + AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{}, + } + // Add to the front since it's most recently updated + allTargets = append([]*pb.Target{newTarget}, allTargets...) + tar = tcp.TestTarget(ctx, t, conn, proj2.GetPublicId(), "test-target-2", target.WithAddress(fmt.Sprintf("127.0.0.11"))) + newTarget = &pb.Target{ + Id: tar.GetPublicId(), + ScopeId: proj2.GetPublicId(), + Name: wrapperspb.String(tar.GetName()), + Scope: &scopes.ScopeInfo{Id: proj2.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org2.GetPublicId()}, + CreatedTime: tar.GetCreateTime().GetTimestamp(), + UpdatedTime: tar.GetUpdateTime().GetTimestamp(), + Version: tar.GetVersion(), + Type: tcp.Subtype.String(), + Attrs: &pb.Target_TcpTargetAttributes{}, + SessionMaxSeconds: wrapperspb.UInt32(28800), + SessionConnectionLimit: wrapperspb.Int32(-1), + AuthorizedActions: testAuthorizedActions, + Address: &wrapperspb.StringValue{Value: fmt.Sprintf("127.0.0.11")}, + } + allTargets = append([]*pb.Target{newTarget}, allTargets...) + + // Leaving this function here as it is very useful if test objects change + /* + printNames := func(step string, tars []*pb.Target) { + names := make([]string, len(tars)) + for i, t := range tars { + names[i] = t.GetName().GetValue() + } + log.Println(step, pretty.Sprint(strings.Join(names, ", "))) + } + */ + + // printNames("before delete ", allTargets) + + // Delete one of the other targets in each project + _, err = repo.DeleteTarget(ctx, allTargets[len(allTargets)-11].Id) + require.NoError(t, err) + deletedTarget1 := allTargets[len(allTargets)-11] + allTargets = append(allTargets[:len(allTargets)-11], allTargets[len(allTargets)-11+1:]...) + // printNames("after first delete ", allTargets) + + _, err = repo.DeleteTarget(ctx, allTargets[len(allTargets)-1].Id) + require.NoError(t, err) + deletedTarget2 := allTargets[len(allTargets)-1] + allTargets = allTargets[:len(allTargets)-1] + // printNames("after second delete", allTargets) + + // Update two of the other targets + allTargets[2].Name = wrapperspb.String("new-name-1") + allTargets[2].Version = 2 + updatedTarget := &tcp.Target{ + Target: &store.Target{ + PublicId: allTargets[2].Id, + Name: allTargets[2].Name.GetValue(), + ProjectId: allTargets[2].ScopeId, + }, + } + tg, _, err := repo.UpdateTarget(ctx, updatedTarget, 1, []string{"name"}) + require.NoError(t, err) + allTargets[2].UpdatedTime = tg.GetUpdateTime().GetTimestamp() + allTargets[2].Version = tg.GetVersion() + // Add to the front since it's most recently updated + newAllTargets := append([]*pb.Target{allTargets[2]}, allTargets[0:2]...) + newAllTargets = append(newAllTargets, allTargets[3:]...) + allTargets = newAllTargets + // printNames("after first update ", allTargets) + allTargets[11].Name = wrapperspb.String("new-name-11") + allTargets[11].Version = 2 + updatedTarget = &tcp.Target{ + Target: &store.Target{ + PublicId: allTargets[11].Id, + Name: allTargets[11].Name.GetValue(), + ProjectId: allTargets[11].ScopeId, + }, + } + tg, _, err = repo.UpdateTarget(ctx, updatedTarget, 1, []string{"name"}) + require.NoError(t, err) + allTargets[11].UpdatedTime = tg.GetUpdateTime().GetTimestamp() + allTargets[11].Version = tg.GetVersion() + // Add to the front since it's most recently updated + newAllTargets = append([]*pb.Target{allTargets[11]}, allTargets[0:11]...) + newAllTargets = append(newAllTargets, allTargets[12:]...) + allTargets = newAllTargets + // printNames("after second update", allTargets) + + // Run analyze to update postgres estimates + _, err = sqlDB.ExecContext(ctx, "analyze") + require.NoError(t, err) + + // Request updated results + req.ListToken = got.ListToken + req.PageSize = 2 + got, err = s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: []*pb.Target{allTargets[0], allTargets[1]}, + ResponseType: "delta", + SortBy: "updated_time", + SortDir: "desc", + // Should contain the deleted target + RemovedIds: []string{deletedTarget1.Id, deletedTarget2.Id}, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) + + // Get next page + req.ListToken = got.ListToken + got, err = s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 2) + // Compare without comparing the list token + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: []*pb.Target{allTargets[2], allTargets[3]}, + ResponseType: "complete", + SortBy: "updated_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) + + // Request new page with filter requiring looping + // to fill the page. + req.ListToken = "" + req.PageSize = 1 + req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, allTargets[len(allTargets)-2].Id, allTargets[len(allTargets)-1].Id) + got, err = s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: []*pb.Target{allTargets[len(allTargets)-2]}, + ResponseType: "delta", + SortBy: "created_time", + SortDir: "desc", + // Should be empty again + RemovedIds: nil, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) + req.ListToken = got.ListToken + // Get the second page + got, err = s.ListTargets(ctx, req) + require.NoError(t, err) + require.Len(t, got.GetItems(), 1) + assert.Empty(t, + cmp.Diff( + got, + &pbs.ListTargetsResponse{ + Items: []*pb.Target{allTargets[len(allTargets)-1]}, + ResponseType: "complete", + SortBy: "created_time", + SortDir: "desc", + RemovedIds: nil, + EstItemCount: 20, + }, + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + protocmp.Transform(), + protocmp.IgnoreFields(&pbs.ListTargetsResponse{}, "list_token"), + ), + ) + + // Create unauthenticated user + unauthAt := authtoken.TestAuthToken(t, conn, kms, org1.GetPublicId()) + unauthR := iam.TestRole(t, conn, proj1.GetPublicId()) + _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) + + // Make a request with the unauthenticated user, + // ensure the response contains the pagination parameters. + requestInfo = authpb.RequestInfo{ + TokenFormat: uint32(auth.AuthTokenTypeBearer), + PublicId: unauthAt.GetPublicId(), + Token: unauthAt.GetToken(), + } + requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) + + got, err = s.ListTargets(ctx, &pbs.ListTargetsRequest{ + ScopeId: "global", + Recursive: true, + }) + require.NoError(t, err) + assert.Empty(t, got.Items) + assert.Equal(t, "created_time", got.SortBy) + assert.Equal(t, "desc", got.SortDir) + assert.Equal(t, "complete", got.ResponseType) + } + + t.Run("with-descendants", func(t *testing.T) { + testListPagination(t, true) + }) + t.Run("without-descendants", func(t *testing.T) { + testListPagination(t, false) }) - require.NoError(t, err) - assert.Empty(t, got.Items) - assert.Equal(t, "created_time", got.SortBy) - assert.Equal(t, "desc", got.SortDir) - assert.Equal(t, "complete", got.ResponseType) } func TestDelete(t *testing.T) { diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 8ca5ac2852..83b09ed97b 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -506,7 +506,7 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva } } - permissions := acl.ListResolvablePermissions(resource.Target, targets.IdActions) + permissions := acl.ListResolvableAliasesPermissions(resource.Target, targets.IdActions) if len(permissions) == 0 { // if there are no permitted targets then there will be no aliases that @@ -615,15 +615,14 @@ func (s Service) aclAndGrantHashForUser(ctx context.Context, userId string) (per // Note: Below, we always skip validation so that we don't error on formats // that we've since restricted, e.g. "ids=foo;actions=create,read". These // will simply not have an effect. - for _, pair := range grantTuples { + for _, tuple := range grantTuples { permsOpts := []perms.Option{ perms.WithUserId(userId), perms.WithSkipFinalValidation(true), } parsed, err := perms.Parse( ctx, - pair.ScopeId, - pair.Grant, + tuple, permsOpts...) if err != nil { return perms.ACL{}, nil, errors.Wrap(ctx, err, op) diff --git a/internal/iam/options.go b/internal/iam/options.go index 7edbebabeb..990da7ace0 100644 --- a/internal/iam/options.go +++ b/internal/iam/options.go @@ -26,22 +26,23 @@ type Option func(*options) // options = how options are represented type options struct { - withPublicId string - withName string - withDescription string - withLimit int - withGrantScopeIds []string - withSkipVetForWrite bool - withDisassociate bool - withSkipAdminRoleCreation bool - withSkipDefaultRoleCreation bool - withUserId string - withRandomReader io.Reader - withAccountIds []string - withPrimaryAuthMethodId string - withReader db.Reader - withWriter db.Writer - withStartPageAfterItem pagination.Item + withPublicId string + withName string + withDescription string + withLimit int + withGrantScopeIds []string + withSkipVetForWrite bool + withDisassociate bool + withSkipAdminRoleCreation bool + withSkipDefaultRoleCreation bool + withUserId string + withRandomReader io.Reader + withAccountIds []string + withPrimaryAuthMethodId string + withReader db.Reader + withWriter db.Writer + withStartPageAfterItem pagination.Item + withTestCacheMultiGrantTuples *[]multiGrantTuple } func getDefaultOptions() options { @@ -175,3 +176,9 @@ func WithStartPageAfterItem(item pagination.Item) Option { o.withStartPageAfterItem = item } } + +func withTestCacheMultiGrantTuples(cache *[]multiGrantTuple) Option { + return func(o *options) { + o.withTestCacheMultiGrantTuples = cache + } +} diff --git a/internal/iam/query.go b/internal/iam/query.go index c5f1dc5808..74af0b713e 100644 --- a/internal/iam/query.go +++ b/internal/iam/query.go @@ -129,15 +129,21 @@ const ( from auth_account where iam_user_id in (select id from users) ), - user_managed_groups (id) as ( + user_oidc_managed_groups (id) as ( select managed_group_id - from auth_managed_group_member_account + from auth_oidc_managed_group_member_account + where member_id in (select id from user_accounts) + ), + user_ldap_managed_groups (id) as ( + select managed_group_id + from auth_ldap_managed_group_member_account where member_id in (select id from user_accounts) ), managed_group_roles (role_id) as ( - select role_id + select distinct role_id from iam_managed_group_role - where principal_id in (select id from user_managed_groups) + where principal_id in (select id from user_oidc_managed_groups) + or principal_id in (select id from user_ldap_managed_groups) ), group_roles (role_id) as ( select role_id @@ -159,83 +165,45 @@ const ( select role_id from managed_group_roles ), - roles (role_id, role_scope_id) as ( + -- Now that we have the role IDs, expand the information to include scope + roles (role_id, role_scope_id, role_parent_scope_id) as ( select iam_role.public_id, - iam_role.scope_id + iam_role.scope_id, + iam_scope.parent_id from iam_role - where public_id in (select role_id from user_group_roles) + join iam_scope + on iam_scope.public_id = iam_role.scope_id + where iam_role.public_id in (select role_id from user_group_roles) ), - role_grant_scopes (role_id, role_scope_id, grant_scope_id) as ( + grant_scopes (role_id, grant_scope_ids) as ( select roles.role_id, - roles.role_scope_id, - iam_role_grant_scope.scope_id_or_special + string_agg(iam_role_grant_scope.scope_id_or_special, '^') as grant_scope_ids from roles - inner join iam_role_grant_scope - on roles.role_id = iam_role_grant_scope.role_id - ), - -- For all role_ids with a special scope_id of 'descendants', we want to - -- perform a cartesian product to pair the role_id with all non-global scopes. - descendant_grant_scopes (role_id, grant_scope_id) as ( - select role_grant_scopes.role_id as role_id, - iam_scope.public_id as grant_scope_id - from role_grant_scopes, - iam_scope - where iam_scope.public_id != 'global' - and role_grant_scopes.grant_scope_id = 'descendants' - ), - children_grant_scopes (role_id, grant_scope_id) as ( - select role_grant_scopes.role_id as role_id, - iam_scope.public_id as grant_scope_id - from role_grant_scopes - join iam_scope - on iam_scope.parent_id = role_grant_scopes.role_scope_id - where role_grant_scopes.grant_scope_id = 'children' + join iam_role_grant_scope + on iam_role_grant_scope.role_id = roles.role_id + group by roles.role_id ), - this_grant_scopes (role_id, grant_scope_id) as ( - select role_grant_scopes.role_id as role_id, - role_grant_scopes.role_scope_id as grant_scope_id - from role_grant_scopes - where role_grant_scopes.grant_scope_id = 'this' - ), - direct_grant_scopes (role_id, grant_scope_id) as ( - select role_grant_scopes.role_id as role_id, - role_grant_scopes.grant_scope_id as grant_scope_id - from role_grant_scopes - where role_grant_scopes.grant_scope_id not in ('descendants', 'children', 'this') - ), - grant_scopes (role_id, grant_scope_id) as ( - select - role_id as role_id, - grant_scope_id as grant_scope_id - from descendant_grant_scopes - union - select - role_id as role_id, - grant_scope_id as grant_scope_id - from children_grant_scopes - union - select - role_id as role_id, - grant_scope_id as grant_scope_id - from this_grant_scopes - union - select - role_id as role_id, - grant_scope_id as grant_scope_id - from direct_grant_scopes - ), - final (role_id, grant_scope_id, canonical_grant) as ( - select grant_scopes.role_id, - grant_scopes.grant_scope_id, - iam_role_grant.canonical_grant - from grant_scopes - join iam_role_grant - on grant_scopes.role_id = iam_role_grant.role_id + grants (role_id, grants) as ( + select roles.role_id, + string_agg(iam_role_grant.canonical_grant, '^') as grants + from roles + join iam_role_grant + on iam_role_grant.role_id = roles.role_id + group by roles.role_id ) - select role_id as role_id, - grant_scope_id as scope_id, - canonical_grant as grant - from final; + -- Finally, take the resulting roles and pull grant scope IDs and canonical grants. + -- We will split these out in application logic to keep the result set size low. + select + roles.role_id as role_id, + roles.role_scope_id as role_scope_id, + roles.role_parent_scope_id as role_parent_scope_id, + grant_scopes.grant_scope_ids as grant_scope_ids, + grants.grants as grants + from roles + join grant_scopes + on grant_scopes.role_id = roles.role_id + join grants + on grants.role_id = roles.role_id; ` estimateCountRoles = ` diff --git a/internal/iam/repository_role_grant.go b/internal/iam/repository_role_grant.go index 116800053e..d9a8d63172 100644 --- a/internal/iam/repository_role_grant.go +++ b/internal/iam/repository_role_grant.go @@ -6,6 +6,8 @@ package iam import ( "context" "fmt" + "sort" + "strings" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" @@ -165,7 +167,7 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe deleteRoleGrants := make([]*RoleGrant, 0, len(grants)) for _, grant := range grants { // Use a fake scope, just want to get out a canonical string - perm, err := perms.Parse(ctx, "o_abcd1234", grant, perms.WithSkipFinalValidation(true)) + perm, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: grant}, perms.WithSkipFinalValidation(true)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string")) } @@ -257,7 +259,7 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi deleteRoleGrants := make([]*RoleGrant, 0, len(grants)) for _, grant := range grants { // Use a fake scope, just want to get out a canonical string - perm, err := perms.Parse(ctx, "o_abcd1234", grant, perms.WithSkipFinalValidation(true)) + perm, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: grant}, perms.WithSkipFinalValidation(true)) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing grant string")) } @@ -410,12 +412,22 @@ func (r *Repository) ListRoleGrantScopes(ctx context.Context, roleIds []string, return roleGrantScopes, nil } -func (r *Repository) GrantsForUser(ctx context.Context, userId string, _ ...Option) (perms.GrantTuples, error) { +type multiGrantTuple struct { + RoleId string + RoleScopeId string + RoleParentScopeId string + GrantScopeIds string + Grants string +} + +func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Option) (perms.GrantTuples, error) { const op = "iam.(Repository).GrantsForUser" if userId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id") } + opts := getOpts(opt...) + const ( anonUser = `where public_id in (?)` authUser = `where public_id in ('u_anon', 'u_auth', ?)` @@ -429,7 +441,7 @@ func (r *Repository) GrantsForUser(ctx context.Context, userId string, _ ...Opti query = fmt.Sprintf(grantsForUserQuery, authUser) } - var grants []perms.GrantTuple + var grants []multiGrantTuple rows, err := r.reader.Query(ctx, query, []any{userId}) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -443,5 +455,42 @@ func (r *Repository) GrantsForUser(ctx context.Context, userId string, _ ...Opti if err := rows.Err(); err != nil { return nil, errors.Wrap(ctx, err, op) } - return grants, nil + + ret := make(perms.GrantTuples, 0, len(grants)*3) + for _, grant := range grants { + for _, grantScopeId := range strings.Split(grant.GrantScopeIds, "^") { + for _, canonicalGrant := range strings.Split(grant.Grants, "^") { + gt := perms.GrantTuple{ + RoleId: grant.RoleId, + RoleScopeId: grant.RoleScopeId, + RoleParentScopeId: grant.RoleParentScopeId, + GrantScopeId: grantScopeId, + Grant: canonicalGrant, + } + if gt.GrantScopeId == globals.GrantScopeThis || gt.GrantScopeId == "" { + gt.GrantScopeId = grant.RoleScopeId + } + ret = append(ret, gt) + } + } + } + + if opts.withTestCacheMultiGrantTuples != nil { + for i, grant := range grants { + grant.testStableSort() + grants[i] = grant + } + *opts.withTestCacheMultiGrantTuples = grants + } + + return ret, nil +} + +func (m *multiGrantTuple) testStableSort() { + grantScopeIds := strings.Split(m.GrantScopeIds, "^") + sort.Strings(grantScopeIds) + m.GrantScopeIds = strings.Join(grantScopeIds, "^") + gts := strings.Split(m.Grants, "^") + sort.Strings(gts) + m.Grants = strings.Join(gts, "^") } diff --git a/internal/iam/repository_role_grant_test.go b/internal/iam/repository_role_grant_test.go index b90216a547..ba90363692 100644 --- a/internal/iam/repository_role_grant_test.go +++ b/internal/iam/repository_role_grant_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "math/rand" + "strings" "testing" "time" @@ -15,6 +16,8 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -138,17 +141,17 @@ func TestRepository_ListRoleGrants(t *testing.T) { opt []Option } tests := []struct { - name string - createCnt int - createScopeId string - args args - wantCnt int - wantErr bool + name string + createCnt int + createGrantScopeId string + args args + wantCnt int + wantErr bool }{ { - name: "no-limit", - createCnt: repo.defaultLimit + 2, - createScopeId: org.PublicId, + name: "no-limit", + createCnt: repo.defaultLimit + 2, + createGrantScopeId: org.PublicId, args: args{ opt: []Option{WithLimit(-1)}, }, @@ -156,9 +159,9 @@ func TestRepository_ListRoleGrants(t *testing.T) { wantErr: false, }, { - name: "no-limit-proj-group", - createCnt: repo.defaultLimit + 2, - createScopeId: proj.PublicId, + name: "no-limit-proj-group", + createCnt: repo.defaultLimit + 2, + createGrantScopeId: proj.PublicId, args: args{ opt: []Option{WithLimit(-1)}, }, @@ -166,16 +169,16 @@ func TestRepository_ListRoleGrants(t *testing.T) { wantErr: false, }, { - name: "default-limit", - createCnt: repo.defaultLimit + 2, - createScopeId: org.PublicId, - wantCnt: repo.defaultLimit, - wantErr: false, + name: "default-limit", + createCnt: repo.defaultLimit + 2, + createGrantScopeId: org.PublicId, + wantCnt: repo.defaultLimit, + wantErr: false, }, { - name: "custom-limit", - createCnt: repo.defaultLimit + 2, - createScopeId: org.PublicId, + name: "custom-limit", + createCnt: repo.defaultLimit + 2, + createGrantScopeId: org.PublicId, args: args{ opt: []Option{WithLimit(3)}, }, @@ -183,9 +186,9 @@ func TestRepository_ListRoleGrants(t *testing.T) { wantErr: false, }, { - name: "bad-role-id", - createCnt: 2, - createScopeId: org.PublicId, + name: "bad-role-id", + createCnt: 2, + createGrantScopeId: org.PublicId, args: args{ withRoleId: "bad-id", }, @@ -197,7 +200,7 @@ func TestRepository_ListRoleGrants(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) db.TestDeleteWhere(t, conn, func() any { r := allocRole(); return &r }(), "1=1") - role := TestRole(t, conn, tt.createScopeId) + role := TestRole(t, conn, tt.createGrantScopeId) roleGrants := make([]string, 0, tt.createCnt) for i := 0; i < tt.createCnt; i++ { roleGrants = append(roleGrants, fmt.Sprintf("ids=h_%d;actions=*", i)) @@ -573,7 +576,6 @@ func TestRepository_SetRoleGrants_Parameters(t *testing.T) { } func TestGrantsForUser(t *testing.T) { - require, assert := require.New(t), assert.New(t) ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") @@ -594,9 +596,9 @@ func TestGrantsForUser(t *testing.T) { WithSkipDefaultRoleCreation(true), ) noGrantOrg1Role := TestRole(t, conn, noGrantOrg1.PublicId) - TestRoleGrant(t, conn, noGrantOrg1Role.PublicId, "ids=o_noGrantOrg1;actions=*") + TestRoleGrant(t, conn, noGrantOrg1Role.PublicId, "ids=*;type=scope;actions=*") noGrantProj1Role := TestRole(t, conn, noGrantProj1.PublicId) - TestRoleGrant(t, conn, noGrantProj1Role.PublicId, "ids=p_noGrantProj1;actions=*") + TestRoleGrant(t, conn, noGrantProj1Role.PublicId, "ids=*;type=*;actions=*") noGrantOrg2, noGrantProj2 := TestScopes( t, repo, @@ -604,9 +606,9 @@ func TestGrantsForUser(t *testing.T) { WithSkipDefaultRoleCreation(true), ) noGrantOrg2Role := TestRole(t, conn, noGrantOrg2.PublicId) - TestRoleGrant(t, conn, noGrantOrg2Role.PublicId, "ids=o_noGrantOrg2;actions=*") + TestRoleGrant(t, conn, noGrantOrg2Role.PublicId, "ids=*;type=scope;actions=*") noGrantProj2Role := TestRole(t, conn, noGrantProj2.PublicId) - TestRoleGrant(t, conn, noGrantProj2Role.PublicId, "ids=p_noGrantProj2;actions=*") + TestRoleGrant(t, conn, noGrantProj2Role.PublicId, "ids=*;type=*;actions=*") // The second org/project set contains direct grants, but without // inheritance. We create two roles in each project. @@ -623,23 +625,22 @@ func TestGrantsForUser(t *testing.T) { WithSkipAdminRoleCreation(true), WithSkipDefaultRoleCreation(true), ) - directGrantOrg1Role := TestRole(t, conn, directGrantOrg1.PublicId, - WithGrantScopeIds([]string{ - globals.GrantScopeThis, - directGrantProj1a.PublicId, - directGrantProj1b.PublicId, - })) + directGrantOrg1Role := TestRole(t, conn, directGrantOrg1.PublicId) TestUserRole(t, conn, directGrantOrg1Role.PublicId, user.PublicId) - directGrantOrg1RoleGrant := "ids=o_directGrantOrg1;actions=*" - TestRoleGrant(t, conn, directGrantOrg1Role.PublicId, directGrantOrg1RoleGrant) + directGrantOrg1RoleGrant1 := "ids=*;type=*;actions=*" + TestRoleGrant(t, conn, directGrantOrg1Role.PublicId, directGrantOrg1RoleGrant1) + directGrantOrg1RoleGrant2 := "ids=*;type=role;actions=list,read" + TestRoleGrant(t, conn, directGrantOrg1Role.PublicId, directGrantOrg1RoleGrant2) + directGrantProj1aRole := TestRole(t, conn, directGrantProj1a.PublicId) TestUserRole(t, conn, directGrantProj1aRole.PublicId, user.PublicId) - directGrantProj1aRoleGrant := "ids=p_directGrantProj1a;actions=*" + directGrantProj1aRoleGrant := "ids=*;type=target;actions=authorize-session,read" TestRoleGrant(t, conn, directGrantProj1aRole.PublicId, directGrantProj1aRoleGrant) directGrantProj1bRole := TestRole(t, conn, directGrantProj1b.PublicId) TestUserRole(t, conn, directGrantProj1bRole.PublicId, user.PublicId) - directGrantProj1bRoleGrant := "ids=p_directGrantProj1b;actions=*" + directGrantProj1bRoleGrant := "ids=*;type=session;actions=list,read" TestRoleGrant(t, conn, directGrantProj1bRole.PublicId, directGrantProj1bRoleGrant) + directGrantOrg2, directGrantProj2a := TestScopes( t, repo, @@ -657,32 +658,27 @@ func TestGrantsForUser(t *testing.T) { WithGrantScopeIds([]string{ globals.GrantScopeThis, directGrantProj2a.PublicId, - directGrantProj2b.PublicId, })) TestUserRole(t, conn, directGrantOrg2Role.PublicId, user.PublicId) - directGrantOrg2RoleGrant := "ids=o_directGrantOrg2;actions=*" - TestRoleGrant(t, conn, directGrantOrg2Role.PublicId, directGrantOrg2RoleGrant) + directGrantOrg2RoleGrant1 := "ids=*;type=user;actions=*" + TestRoleGrant(t, conn, directGrantOrg2Role.PublicId, directGrantOrg2RoleGrant1) + directGrantOrg2RoleGrant2 := "ids=*;type=group;actions=list,read" + TestRoleGrant(t, conn, directGrantOrg2Role.PublicId, directGrantOrg2RoleGrant2) + directGrantProj2aRole := TestRole(t, conn, directGrantProj2a.PublicId) TestUserRole(t, conn, directGrantProj2aRole.PublicId, user.PublicId) - directGrantProj2aRoleGrant := "ids=p_directGrantProj2a;actions=*" + directGrantProj2aRoleGrant := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" TestRoleGrant(t, conn, directGrantProj2aRole.PublicId, directGrantProj2aRoleGrant) directGrantProj2bRole := TestRole(t, conn, directGrantProj2b.PublicId) TestUserRole(t, conn, directGrantProj2bRole.PublicId, user.PublicId) - directGrantProj2bRoleGrant := "ids=p_directGrantProj2b;actions=*" + directGrantProj2bRoleGrant := "ids=cs_abcd1234;actions=read,update" TestRoleGrant(t, conn, directGrantProj2bRole.PublicId, directGrantProj2bRoleGrant) // For the third set we create a couple of orgs/projects and then use - // "children". We expect to see no grant on the org but for both projects. - childGrantOrg1, childGrantProj1a := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) - childGrantProj1b := TestProject( + // globals.GrantScopeChildren. + childGrantOrg1, childGrantOrg1Proj := TestScopes( t, repo, - childGrantOrg1.PublicId, WithSkipAdminRoleCreation(true), WithSkipDefaultRoleCreation(true), ) @@ -691,28 +687,23 @@ func TestGrantsForUser(t *testing.T) { globals.GrantScopeChildren, })) TestUserRole(t, conn, childGrantOrg1Role.PublicId, user.PublicId) - childGrantOrg1RoleGrant := "ids=o_childGrantOrg1;actions=*" + childGrantOrg1RoleGrant := "ids=*;type=host-set;actions=add-hosts,remove-hosts" TestRoleGrant(t, conn, childGrantOrg1Role.PublicId, childGrantOrg1RoleGrant) - childGrantOrg2, childGrantProj2a := TestScopes( + childGrantOrg2, childGrantOrg2Proj := TestScopes( t, repo, WithSkipAdminRoleCreation(true), WithSkipDefaultRoleCreation(true), ) - childGrantProj2b := TestProject( - t, - repo, - childGrantOrg2.PublicId, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) childGrantOrg2Role := TestRole(t, conn, childGrantOrg2.PublicId, WithGrantScopeIds([]string{ globals.GrantScopeChildren, })) TestUserRole(t, conn, childGrantOrg2Role.PublicId, user.PublicId) - childGrantOrg2RoleGrant := "ids=o_childGrantOrg2;actions=*" - TestRoleGrant(t, conn, childGrantOrg2Role.PublicId, childGrantOrg2RoleGrant) + childGrantOrg2RoleGrant1 := "ids=*;type=session;actions=cancel:self" + TestRoleGrant(t, conn, childGrantOrg2Role.PublicId, childGrantOrg2RoleGrant1) + childGrantOrg2RoleGrant2 := "ids=*;type=session;actions=read:self" + TestRoleGrant(t, conn, childGrantOrg2Role.PublicId, childGrantOrg2RoleGrant2) // Finally, let's create some roles at global scope with children and // descendants grants @@ -721,7 +712,7 @@ func TestGrantsForUser(t *testing.T) { globals.GrantScopeChildren, })) TestUserRole(t, conn, childGrantGlobalRole.PublicId, globals.AnyAuthenticatedUserId) - childGrantGlobalRoleGrant := "ids=*;type=host;actions=*" + childGrantGlobalRoleGrant := "ids=*;type=account;actions=*" TestRoleGrant(t, conn, childGrantGlobalRole.PublicId, childGrantGlobalRoleGrant) descendantGrantGlobalRole := TestRole(t, conn, scope.Global.String(), WithGrantScopeIds([]string{ @@ -731,235 +722,817 @@ func TestGrantsForUser(t *testing.T) { descendantGrantGlobalRoleGrant := "ids=*;type=credential;actions=*" TestRoleGrant(t, conn, descendantGrantGlobalRole.PublicId, descendantGrantGlobalRoleGrant) - /* - // Useful if needing to debug - t.Log( - "\nnoGrantOrg1", noGrantOrg1.PublicId, noGrantOrg1Role.PublicId, - "\nnoGrantProj1", noGrantProj1.PublicId, noGrantProj1Role.PublicId, - "\nnoGrantOrg2", noGrantOrg2.PublicId, noGrantOrg2Role.PublicId, - "\nnoGrantProj2", noGrantProj2.PublicId, noGrantProj2Role.PublicId, - "\ndirectGrantOrg1", directGrantOrg1.PublicId, directGrantOrg1Role.PublicId, - "\ndirectGrantProj1a", directGrantProj1a.PublicId, directGrantProj1aRole.PublicId, - "\ndirectGrantProj1b", directGrantProj1b.PublicId, directGrantProj1bRole.PublicId, - "\ndirectGrantOrg2", directGrantOrg2.PublicId, directGrantOrg2Role.PublicId, - "\ndirectGrantProj2a", directGrantProj2a.PublicId, directGrantProj2aRole.PublicId, - "\ndirectGrantProj2b", directGrantProj2b.PublicId, directGrantProj2bRole.PublicId, - "\nchildGrantOrg1", childGrantOrg1.PublicId, childGrantOrg1Role.PublicId, - "\nchildGrantProj1a", childGrantProj1a.PublicId, - "\nchildGrantProj1b", childGrantProj1b.PublicId, - "\nchildGrantOrg2", childGrantOrg2.PublicId, childGrantOrg2Role.PublicId, - "\nchildGrantProj2a", childGrantProj2a.PublicId, - "\nchildGrantProj2b", childGrantProj2b.PublicId, - "\nchildGrantGlobalRole", childGrantGlobalRole.PublicId, - "\ndescendantGrantGlobalRole", descendantGrantGlobalRole.PublicId, - ) - */ - - // We expect to see: - // - // * No grants from noOrg/noProj - // * Grants from direct orgs/projs: - // * directGrantOrg1/directGrantOrg2 on org and respective projects (6 grants total) - // * directGrantProj on respective projects (4 grants total) - expGrantTuples := []perms.GrantTuple{ - // No grants from noOrg/noProj - // Grants from direct org1 to org1/proj1a/proj1b: - { - RoleId: directGrantOrg1Role.PublicId, - ScopeId: directGrantOrg1.PublicId, - Grant: directGrantOrg1RoleGrant, - }, - { - RoleId: directGrantOrg1Role.PublicId, - ScopeId: directGrantProj1a.PublicId, - Grant: directGrantOrg1RoleGrant, - }, - { - RoleId: directGrantOrg1Role.PublicId, - ScopeId: directGrantProj1b.PublicId, - Grant: directGrantOrg1RoleGrant, - }, - // Grants from direct org 1 proj 1a: - { - RoleId: directGrantProj1aRole.PublicId, - ScopeId: directGrantProj1a.PublicId, - Grant: directGrantProj1aRoleGrant, - }, - // Grant from direct org 1 proj 1 b: - { - RoleId: directGrantProj1bRole.PublicId, - ScopeId: directGrantProj1b.PublicId, - Grant: directGrantProj1bRoleGrant, - }, + t.Run("db-grants", func(t *testing.T) { + // Here we should see exactly what the DB has returned, before we do some + // local exploding of grants and grant scopes + expMultiGrantTuples := []multiGrantTuple{ + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: strings.Join([]string{directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2}, "^"), + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: strings.Join([]string{directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2}, "^"), + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant, + }, + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant, + }, + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant, + }, + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: strings.Join([]string{childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2}, "^"), + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant, + }, + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant, + }, + } + for i, tuple := range expMultiGrantTuples { + tuple.testStableSort() + expMultiGrantTuples[i] = tuple + } + multiGrantTuplesCache := new([]multiGrantTuple) + _, err := repo.GrantsForUser(ctx, user.PublicId, withTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) - // Grants from direct org1 to org2/proj2a/proj2b: - { - RoleId: directGrantOrg2Role.PublicId, - ScopeId: directGrantOrg2.PublicId, - Grant: directGrantOrg2RoleGrant, - }, - { - RoleId: directGrantOrg2Role.PublicId, - ScopeId: directGrantProj2a.PublicId, - Grant: directGrantOrg2RoleGrant, - }, - { - RoleId: directGrantOrg2Role.PublicId, - ScopeId: directGrantProj2b.PublicId, - Grant: directGrantOrg2RoleGrant, - }, - // Grants from direct org 2 proj 2a: - { - RoleId: directGrantProj2aRole.PublicId, - ScopeId: directGrantProj2a.PublicId, - Grant: directGrantProj2aRoleGrant, - }, - // Grant from direct org 2 proj 2 b: - { - RoleId: directGrantProj2bRole.PublicId, - ScopeId: directGrantProj2b.PublicId, - Grant: directGrantProj2bRoleGrant, - }, + // log.Println("multiGrantTuplesCache", pretty.Sprint(*multiGrantTuplesCache)) + assert.ElementsMatch(t, *multiGrantTuplesCache, expMultiGrantTuples) + }) - // Child grants from child org1 to proj1a/proj1b: - { - RoleId: childGrantOrg1Role.PublicId, - ScopeId: childGrantProj1a.PublicId, - Grant: childGrantOrg1RoleGrant, - }, - { - RoleId: childGrantOrg1Role.PublicId, - ScopeId: childGrantProj1b.PublicId, - Grant: childGrantOrg1RoleGrant, - }, - // Child grants from child org2 to proj2a/proj2b: - { - RoleId: childGrantOrg2Role.PublicId, - ScopeId: childGrantProj2a.PublicId, - Grant: childGrantOrg2RoleGrant, - }, - { - RoleId: childGrantOrg2Role.PublicId, - ScopeId: childGrantProj2b.PublicId, - Grant: childGrantOrg2RoleGrant, - }, + t.Run("exploded-grants", func(t *testing.T) { + // We expect to see: + // + // * No grants from noOrg/noProj + // * Grants from direct orgs/projs: + // * directGrantOrg1/directGrantOrg2 on org and respective projects (6 grants total per org) + // * directGrantProj on respective projects (4 grants total) + expGrantTuples := []perms.GrantTuple{ + // No grants from noOrg/noProj + // Grants from direct org1 to org1/proj1a/proj1b: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant1, + }, + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant2, + }, + // Grants from direct org 1 proj 1a: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Grant: directGrantProj1aRoleGrant, + }, + // Grant from direct org 1 proj 1 b: + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Grant: directGrantProj1bRoleGrant, + }, - // Grants from global to every org: - { - RoleId: childGrantGlobalRole.PublicId, - ScopeId: noGrantOrg1.PublicId, - Grant: childGrantGlobalRoleGrant, - }, - { - RoleId: childGrantGlobalRole.PublicId, - ScopeId: noGrantOrg2.PublicId, - Grant: childGrantGlobalRoleGrant, - }, - { - RoleId: childGrantGlobalRole.PublicId, - ScopeId: directGrantOrg1.PublicId, - Grant: childGrantGlobalRoleGrant, - }, - { - RoleId: childGrantGlobalRole.PublicId, - ScopeId: directGrantOrg2.PublicId, - Grant: childGrantGlobalRoleGrant, - }, - { - RoleId: childGrantGlobalRole.PublicId, - ScopeId: childGrantOrg1.PublicId, - Grant: childGrantGlobalRoleGrant, - }, - { - RoleId: childGrantGlobalRole.PublicId, - ScopeId: childGrantOrg2.PublicId, - Grant: childGrantGlobalRoleGrant, - }, + // Grants from direct org2 to org2/proj2a/proj2b: + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + // Grants from direct org 2 proj 2a: + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantProj2aRoleGrant, + }, + // Grant from direct org 2 proj 2 b: + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Grant: directGrantProj2bRoleGrant, + }, + // Child grants from child org1 to proj1a/proj1b: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg1RoleGrant, + }, + // Child grants from child org2 to proj2a/proj2b: + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant1, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant2, + }, - // Grants from global to every org and project: - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: noGrantOrg1.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: noGrantProj1.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: noGrantOrg2.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: noGrantProj2.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: directGrantOrg1.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: directGrantProj1a.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: directGrantProj1b.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: directGrantOrg2.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: directGrantProj2a.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: directGrantProj2b.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: childGrantOrg1.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, + // Grants from global to every org: + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantGlobalRoleGrant, + }, + + // Grants from global to every org and project: + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Grant: descendantGrantGlobalRoleGrant, + }, + } + + multiGrantTuplesCache := new([]multiGrantTuple) + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, withTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + assert.ElementsMatch(t, grantTuples, expGrantTuples) + }) + + t.Run("acl-grants", func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + + t.Run("descendant-grants", func(t *testing.T) { + descendantGrants := acl.DescendantsGrants() + expDescendantGrants := []perms.AclGrant{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Id: "*", + Type: resource.Credential, + ActionSet: perms.ActionSet{action.All: true}, + }, + } + assert.ElementsMatch(t, descendantGrants, expDescendantGrants) + }) + + t.Run("child-grants", func(t *testing.T) { + childrenGrants := acl.ChildrenScopeGrantMap() + expChildrenGrants := map[string][]perms.AclGrant{ + childGrantOrg1.PublicId: { + { + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.HostSet, + ActionSet: perms.ActionSet{action.AddHosts: true, action.RemoveHosts: true}, + }, + }, + childGrantOrg2.PublicId: { + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Session, + ActionSet: perms.ActionSet{action.CancelSelf: true}, + }, + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Session, + ActionSet: perms.ActionSet{action.ReadSelf: true}, + }, + }, + scope.Global.String(): { + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Account, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + } + assert.Len(t, childrenGrants, len(expChildrenGrants)) + for k, v := range childrenGrants { + assert.ElementsMatch(t, v, expChildrenGrants[k]) + } + }) + + t.Run("direct-grants", func(t *testing.T) { + directGrants := acl.DirectScopeGrantMap() + expDirectGrants := map[string][]perms.AclGrant{ + directGrantOrg1.PublicId: { + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.All, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Role, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj1a.PublicId: { + { + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Id: "*", + Type: resource.Target, + ActionSet: perms.ActionSet{action.AuthorizeSession: true, action.Read: true}, + }, + }, + directGrantProj1b.PublicId: { + { + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Id: "*", + Type: resource.Session, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantOrg2.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.User, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj2a.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.User, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_1234abcd", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + directGrantProj2b.PublicId: { + { + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.Update: true, action.Read: true}, + }, + }, + } + /* + log.Println("org1", directGrantOrg1.PublicId) + log.Println("proj1a", directGrantProj1a.PublicId) + log.Println("proj1b", directGrantProj1b.PublicId) + log.Println("org2", directGrantOrg2.PublicId) + log.Println("proj2a", directGrantProj2a.PublicId) + log.Println("proj2b", directGrantProj2b.PublicId) + */ + assert.Len(t, directGrants, len(expDirectGrants)) + for k, v := range directGrants { + assert.ElementsMatch(t, v, expDirectGrants[k]) + } + }) + }) + t.Run("real-world", func(t *testing.T) { + // These tests cases crib from the initial setup of the grants, and + // include a number of cases to ensure the ones that should work do and + // various that should not do not + type testCase struct { + name string + res perms.Resource + act action.Type + shouldWork bool + } + testCases := []testCase{} + + // These test cases should fail because the grants are in roles where + // the user is not a principal { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: childGrantProj1a.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, + testCases = append(testCases, testCase{ + name: "nogrant-a", + res: perms.Resource{ + ScopeId: noGrantOrg1.PublicId, + Id: "u_abcd1234", + Type: resource.Scope, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + }, testCase{ + name: "nogrant-b", + res: perms.Resource{ + ScopeId: noGrantProj1.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: noGrantOrg1.String(), + }, + act: action.Read, + }, testCase{ + name: "nogrant-c", + res: perms.Resource{ + ScopeId: noGrantOrg2.PublicId, + Id: "u_abcd1234", + Type: resource.Scope, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + }, testCase{ + name: "nogrant-d", + res: perms.Resource{ + ScopeId: noGrantProj2.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: noGrantOrg2.String(), + }, + act: action.Read, + }, + ) + } + // These test cases are for org1 and its projects where the grants are + // direct, not via children/descendants. They test some actions that + // should work and some that shouldn't. { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: childGrantProj1b.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, + testCases = append(testCases, testCase{ + name: "direct-a", + res: perms.Resource{ + ScopeId: directGrantOrg1.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-b", + res: perms.Resource{ + ScopeId: directGrantOrg1.PublicId, + Id: "r_abcd1234", + Type: resource.Role, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-c", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "ttcp_abcd1234", + Type: resource.Target, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.AuthorizeSession, + shouldWork: true, + }, testCase{ + name: "direct-d", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Read, + }, testCase{ + name: "direct-e", + res: perms.Resource{ + ScopeId: directGrantProj1b.PublicId, + Id: "ttcp_abcd1234", + Type: resource.Target, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.AuthorizeSession, + }, testCase{ + name: "direct-f", + res: perms.Resource{ + ScopeId: directGrantProj1b.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Read, + shouldWork: true, + }, + ) + } + // These test cases are for org2 and its projects where the grants are + // direct, not via children/descendants. They test some actions that + // should work and some that shouldn't. { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: childGrantOrg2.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, + testCases = append(testCases, testCase{ + name: "direct-g", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "direct-m", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "g_abcd1234", + Type: resource.Group, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + }, testCase{ + name: "direct-h", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "acct_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Delete, + shouldWork: true, + }, testCase{ + name: "direct-i", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Type: resource.Group, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.List, + shouldWork: true, + }, testCase{ + name: "direct-j", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Id: "r_abcd1234", + Type: resource.Role, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Read, + }, testCase{ + name: "direct-n", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-k", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.HostCatalog, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-l", + res: perms.Resource{ + ScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.CredentialStore, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Update, + shouldWork: true, + }, + testCase{ + name: "direct-m", + res: perms.Resource{ + ScopeId: directGrantProj2b.PublicId, + Id: "cl_abcd1234", + Type: resource.CredentialLibrary, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Update, + }, + ) + } + // These test cases are child grants { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: childGrantProj2a.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, + testCases = append(testCases, testCase{ + name: "children-a", + res: perms.Resource{ + ScopeId: scope.Global.String(), + Id: "a_abcd1234", + Type: resource.Account, + }, + act: action.Update, + }, testCase{ + name: "children-b", + res: perms.Resource{ + ScopeId: noGrantOrg1.PublicId, + Id: "a_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "children-c", + res: perms.Resource{ + ScopeId: directGrantOrg1.PublicId, + Id: "a_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "children-d", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "a_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "children-e", + res: perms.Resource{ + ScopeId: childGrantOrg2.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: scope.Global.String(), + }, + act: action.CancelSelf, + }, testCase{ + name: "children-f", + res: perms.Resource{ + ScopeId: childGrantOrg1Proj.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: childGrantOrg1.PublicId, + }, + act: action.CancelSelf, + }, testCase{ + name: "children-g", + res: perms.Resource{ + ScopeId: childGrantOrg2Proj.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: childGrantOrg2.PublicId, + }, + act: action.CancelSelf, + shouldWork: true, + }, testCase{ + name: "children-h", + res: perms.Resource{ + ScopeId: childGrantOrg2Proj.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: childGrantOrg2.PublicId, + }, + act: action.CancelSelf, + shouldWork: true, + }, testCase{ + name: "children-i", + res: perms.Resource{ + ScopeId: childGrantOrg1.PublicId, + Id: "hsst_abcd1234", + Type: resource.HostSet, + ParentScopeId: scope.Global.String(), + }, + act: action.AddHosts, + }, testCase{ + name: "children-j", + res: perms.Resource{ + ScopeId: childGrantOrg1Proj.PublicId, + Id: "hsst_abcd1234", + Type: resource.HostSet, + ParentScopeId: childGrantOrg1.PublicId, + }, + act: action.AddHosts, + shouldWork: true, + }, testCase{ + name: "children-k", + res: perms.Resource{ + ScopeId: childGrantOrg2Proj.PublicId, + Id: "hsst_abcd1234", + Type: resource.HostSet, + ParentScopeId: childGrantOrg2.PublicId, + }, + act: action.AddHosts, + }, + ) + } + // These test cases are global descendants grants { - RoleId: descendantGrantGlobalRole.PublicId, - ScopeId: childGrantProj2b.PublicId, - Grant: descendantGrantGlobalRoleGrant, - }, - } - - grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) - require.NoError(err) - assert.ElementsMatch(grantTuples, expGrantTuples) + testCases = append(testCases, testCase{ + name: "descendants-a", + res: perms.Resource{ + ScopeId: scope.Global.String(), + Id: "cs_abcd1234", + Type: resource.Credential, + }, + act: action.Update, + }, testCase{ + name: "descendants-b", + res: perms.Resource{ + ScopeId: noGrantProj1.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: noGrantOrg1.PublicId, + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-c", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-d", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-e", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-f", + res: perms.Resource{ + ScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Update, + shouldWork: true, + }, + ) + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + assert.True(t, acl.Allowed(tc.res, tc.act, "u_abc123").Authorized == tc.shouldWork) + }) + } + }) } diff --git a/internal/iam/role_grant.go b/internal/iam/role_grant.go index c84ad0d1b9..1e991ca8ca 100644 --- a/internal/iam/role_grant.go +++ b/internal/iam/role_grant.go @@ -40,7 +40,7 @@ func NewRoleGrant(ctx context.Context, roleId string, grant string, _ ...Option) // Validate that the grant parses successfully. Note that we fake the scope // here to avoid a lookup as the scope is only relevant at actual ACL // checking time and we just care that it parses correctly. - perm, err := perms.Parse(ctx, "o_abcd1234", grant) + perm, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: grant}) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string")) } @@ -80,7 +80,7 @@ func (g *RoleGrant) VetForWrite(ctx context.Context, _ db.Reader, _ db.OpType, _ // checking time and we just care that it parses correctly. We may have // already done this in NewRoleGrant, but we re-check and set it here // anyways because it should still be part of the vetting process. - perm, err := perms.Parse(ctx, "o_abcd1234", g.RawGrant) + perm, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: g.RawGrant}) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string")) } diff --git a/internal/perms/acl.go b/internal/perms/acl.go index db006e3b36..2f6037e001 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -9,23 +9,30 @@ import ( "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" ) // AclGrant is used to decouple API-based grants from those we utilize for ACLs. // Notably it uses a single ID per grant instead of multiple IDs. type AclGrant struct { - // The scope ID, which will be a project ID or an org ID - scope Scope + // The scope ID of the role that sourced this grant + RoleScopeId string + + // The parent scope ID of the role that sourced this grant + RoleParentScopeId string + + // The grant's applied scope ID + GrantScopeId string // The ID to use - id string + Id string // The type, if provided - typ resource.Type + Type resource.Type // The set of actions being granted - actions actionSet + ActionSet ActionSet // The set of output fields granted OutputFields *OutputFields @@ -33,14 +40,48 @@ type AclGrant struct { // Actions returns the actions as a slice from the internal map, along with the // string representations of those actions. -func (a AclGrant) Actions() ([]action.Type, []string) { - return a.actions.Actions() +func (ag AclGrant) Actions() ([]action.Type, []string) { + return ag.ActionSet.Actions() +} + +func (ag AclGrant) Clone() AclGrant { + ret := AclGrant{ + RoleScopeId: ag.RoleScopeId, + RoleParentScopeId: ag.RoleParentScopeId, + GrantScopeId: ag.GrantScopeId, + Id: ag.Id, + Type: ag.Type, + } + if ag.ActionSet != nil { + ret.ActionSet = make(map[action.Type]bool, len(ag.ActionSet)) + for k, v := range ag.ActionSet { + ret.ActionSet[k] = v + } + } + if ag.OutputFields != nil { + ret.OutputFields = new(OutputFields) + ret.OutputFields.fields = make(map[string]bool, len(ag.OutputFields.fields)) + for k, v := range ag.OutputFields.fields { + ret.OutputFields.fields[k] = v + } + } + return ret } // ACL provides an entry point into the permissions engine for determining if an -// action is allowed on a resource based on a principal's (user or group) grants. +// action is allowed on a resource based on a principal's (user or group) +// grants. type ACL struct { - scopeMap map[string][]AclGrant + // directScopeMap is a map of scope IDs to grants valid for that scope ID + // where the grant scope ID was specified directly + directScopeMap map[string][]AclGrant + // childrenScopeMap is a map of _parent_ scope IDs to grants, so that when + // we are checking a resource we can see if there were any "children" grant + // scope IDs that match + childrenScopeMap map[string][]AclGrant + // descendantsGrants is a list of grants that apply to all descendants of + // global + descendantsGrants []AclGrant } // ACLResults provides a type for the permission's engine results so that we can @@ -52,15 +93,19 @@ type ACLResults struct { OutputFields *OutputFields // This is included but unexported for testing/debugging - scopeMap map[string][]AclGrant + directScopeMap map[string][]AclGrant + childrenScopeMap map[string][]AclGrant + descendantsGrants []AclGrant } // Permission provides information about the specific // resources that a user has been granted access to for a given scope, resource, and action. type Permission struct { - ScopeId string // The scope id for which the permission applies. - Resource resource.Type - Action action.Type + RoleScopeId string // The scope id of the granting role + RoleParentScopeId string // The parent scope id of the granting role + GrantScopeId string // Same as the scope ID unless "children" or "descendants" was used. + Resource resource.Type + Action action.Type ResourceIds []string // Any specific resource ids that have been referred in the grant's `id` field, if applicable. OnlySelf bool // The grant only allows actions against the user's own resources. @@ -88,37 +133,85 @@ type Resource struct { // Pin if defined would constrain the resource within the collection of the // pin id. Pin string `json:"pin,omitempty"` + + // ParentScopeId is the parent scope of the resource. + ParentScopeId string `json:"-"` } // NewACL creates an ACL from the grants provided. Note that this converts the // API-based Grants to AclGrants. func NewACL(grants ...Grant) ACL { ret := ACL{ - scopeMap: make(map[string][]AclGrant, len(grants)), + directScopeMap: make(map[string][]AclGrant, len(grants)), + childrenScopeMap: make(map[string][]AclGrant, len(grants)), + descendantsGrants: make([]AclGrant, 0, len(grants)), } for _, grant := range grants { - switch { - case len(grant.ids) > 0: - for _, id := range grant.ids { - ret.scopeMap[grant.scope.Id] = append(ret.scopeMap[grant.scope.Id], aclGrantFromGrant(grant, id)) - } - default: + ids := grant.ids + if len(ids) == 0 { // This handles the no-ID case as well as the deprecated single-ID case - ret.scopeMap[grant.scope.Id] = append(ret.scopeMap[grant.scope.Id], aclGrantFromGrant(grant, grant.id)) + ids = []string{grant.id} + } + for _, id := range ids { + switch grant.grantScopeId { + case globals.GrantScopeDescendants: + ret.descendantsGrants = append(ret.descendantsGrants, aclGrantFromGrant(grant, id)) + case globals.GrantScopeChildren: + // We use the role's scope here because we're evaluating the + // grants themselves, not the resource, so we want to know the + // scope of the role that said "children" + ret.childrenScopeMap[grant.roleScopeId] = append(ret.childrenScopeMap[grant.roleScopeId], aclGrantFromGrant(grant, id)) + default: + ret.directScopeMap[grant.grantScopeId] = append(ret.directScopeMap[grant.grantScopeId], aclGrantFromGrant(grant, id)) + } } } return ret } +func (a ACL) DirectScopeGrantMap() map[string][]AclGrant { + ret := make(map[string][]AclGrant, len(a.directScopeMap)) + for k, v := range a.directScopeMap { + newSlice := make([]AclGrant, len(v)) + for i, g := range v { + newSlice[i] = g.Clone() + } + ret[k] = newSlice + } + return ret +} + +func (a ACL) ChildrenScopeGrantMap() map[string][]AclGrant { + ret := make(map[string][]AclGrant, len(a.childrenScopeMap)) + for k, v := range a.childrenScopeMap { + newSlice := make([]AclGrant, len(v)) + for i, g := range v { + newSlice[i] = g.Clone() + } + ret[k] = newSlice + } + return ret +} + +func (a ACL) DescendantsGrants() []AclGrant { + ret := make([]AclGrant, len(a.descendantsGrants)) + for i, v := range a.descendantsGrants { + ret[i] = v.Clone() + } + return ret +} + func aclGrantFromGrant(grant Grant, id string) AclGrant { return AclGrant{ - scope: grant.scope, - id: id, - typ: grant.typ, - actions: grant.actions, - OutputFields: grant.OutputFields, + RoleScopeId: grant.roleScopeId, + RoleParentScopeId: grant.roleParentScopeId, + GrantScopeId: grant.grantScopeId, + Id: id, + Type: grant.typ, + ActionSet: grant.actions, + OutputFields: grant.OutputFields, } } @@ -126,9 +219,16 @@ func aclGrantFromGrant(grant Grant, id string) AclGrant { func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option) (results ACLResults) { opts := getOpts(opt...) - // First, get the grants within the specified scope - grants := a.scopeMap[r.ScopeId] - results.scopeMap = a.scopeMap + // First, get the grants within the specified scopes + grants := a.directScopeMap[r.ScopeId] + grants = append(grants, a.childrenScopeMap[r.ParentScopeId]...) + if r.ScopeId != scope.Global.String() { + // Descendants grants do not apply to global! + grants = append(grants, a.descendantsGrants...) + } + results.directScopeMap = a.directScopeMap + results.childrenScopeMap = a.childrenScopeMap + results.descendantsGrants = a.descendantsGrants var parentAction action.Type split := strings.Split(aType.String(), ":") @@ -139,7 +239,7 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option for _, grant := range grants { var outputFieldsOnly bool switch { - case len(grant.actions) == 0: + case len(grant.ActionSet) == 0: // Continue with the next grant, unless we have output fields // specified in which case we continue to be able to apply the // output fields depending on ID and type. @@ -148,13 +248,13 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option } else { continue } - case grant.actions[aType]: + case grant.ActionSet[aType]: // We have this action - case grant.actions[parentAction]: + case grant.ActionSet[parentAction]: // We don't have this action, but it's a subaction and we have the // parent action. As an example, if we are looking for "read:self" // and have "read", this is sufficient. - case grant.actions[action.All]: + case grant.ActionSet[action.All]: // All actions are allowed default: // No actions in the grant match what we're looking for, so continue @@ -188,35 +288,35 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option switch { // Allow discovery of scopes, so that auth methods within can be // discovered - case grant.typ == r.Type && - grant.typ == resource.Scope && + case grant.Type == r.Type && + grant.Type == resource.Scope && (aType == action.List || aType == action.NoOp): found = true // Allow discovery of and authenticating to auth methods - case grant.typ == r.Type && - grant.typ == resource.AuthMethod && + case grant.Type == r.Type && + grant.Type == resource.AuthMethod && (aType == action.List || aType == action.NoOp || aType == action.Authenticate): found = true } // Case 2: - // id=;actions= where ID cannot be a wildcard; or - // id=;output_fields= where fields cannot be a + // id=;actions= where ID cannot be a wildcard; or + // id=;output_fields= where fields cannot be a // wildcard. - case grant.id == r.Id && - grant.id != "" && - grant.id != "*" && - (grant.typ == resource.Unknown || grant.typ == globals.ResourceInfoFromPrefix(grant.id).Type) && + case grant.Id == r.Id && + grant.Id != "" && + grant.Id != "*" && + (grant.Type == resource.Unknown || grant.Type == globals.ResourceInfoFromPrefix(grant.Id).Type) && !action.List.IsActionOrParent(aType) && !action.Create.IsActionOrParent(aType): found = true - // Case 3: type=;actions= when action is list or + // Case 3: type=;actions= when action is list or // create (cannot be a wildcard). Must be a top level collection, // otherwise must be one of the two formats specified in cases 4 or 5. - // Or, type=resource.type;output_fields= and no action. This is + // Or, type=resource.Type;output_fields= and no action. This is // more of a semantic difference compared to 4 more than a security // difference; this type is for clarity as it ties more closely to the // concept of create and list as actions on a collection, operating on a @@ -228,10 +328,10 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option // "two ways of doing things" but it's a reasonable UX tradeoff given // that "all IDs" can reasonably be construed to include "and the one // I'm making" and "all of them for listing". - case grant.id == "" && + case grant.Id == "" && r.Id == "" && - grant.typ == r.Type && - grant.typ != resource.Unknown && + grant.Type == r.Type && + grant.Type != resource.Unknown && resource.TopLevelType(r.Type) && (action.List.IsActionOrParent(aType) || action.Create.IsActionOrParent(aType)): @@ -239,24 +339,24 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option found = true // Case 4: - // id=*;type=;actions= where type cannot be + // id=*;type=;actions= where type cannot be // unknown but can be a wildcard to allow any resource at all; or - // id=*;type=;output_fields= with no action. - case grant.id == "*" && - grant.typ != resource.Unknown && - (grant.typ == r.Type || - grant.typ == resource.All): + // id=*;type=;output_fields= with no action. + case grant.Id == "*" && + grant.Type != resource.Unknown && + (grant.Type == r.Type || + grant.Type == resource.All): found = true // Case 5: - // id=;type=;actions= where type can be a + // id=;type=;actions= where type can be a // wildcard and this this is operating on a non-top-level type. Same for // output fields only. - case grant.id != "" && - grant.id == r.Pin && - grant.typ != resource.Unknown && - (grant.typ == r.Type || grant.typ == resource.All) && + case grant.Id != "" && + grant.Id == r.Pin && + grant.Type != resource.Unknown && + (grant.Type == r.Type || grant.Type == resource.All) && !resource.TopLevelType(r.Type): found = true @@ -276,25 +376,110 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option return } -// ListResolvablePermissions builds a set of Permissions based on the grants in -// the ACL. The permissions will only be created if there is at least +// ListResolvableAliasesPermissions builds a set of Permissions based on the +// grants in the ACL. The permissions will only be created if there is at least // one grant of the provided resource type that includes at least one of the -// provided actions in the action set. -// Note that unlike the ListPermissions method, this method does not attempt to -// generate permissions for the u_recovery user. To get the resolvable aliases -// for u_recovery, the user could simply query all aliases with a destination id. -func (a ACL) ListResolvablePermissions(requestedType resource.Type, actions action.ActionSet) []Permission { - perms := make([]Permission, 0, len(a.scopeMap)) - for scopeId := range a.scopeMap { - // Consider all scopes in the grants. They may not exist, but if that is - // the case the +// provided actions in the action set. Note that unlike the ListPermissions +// method, this method does not attempt to generate permissions for the +// u_recovery user. To get the resolvable aliases for u_recovery, the user could +// simply query all aliases with a destination id. +func (a ACL) ListResolvableAliasesPermissions(requestedType resource.Type, actions action.ActionSet) []Permission { + perms := make([]Permission, 0, len(a.directScopeMap)+len(a.childrenScopeMap)+len(a.descendantsGrants)) + + childScopeMap := a.childrenScopeMap + scopeMap := a.directScopeMap + + // Unilaterally add the descendants grants, if any. Not specifying an Id or + // ParentScopeId in ScopeInfo means that the only grants that might match + // are descendants, and we tell buildPermission to include descendants. + p := Permission{ + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Resource: requestedType, + Action: action.ListResolvableAliases, + OnlySelf: true, // default to only self to be restrictive + } + if a.buildPermission(&scopes.ScopeInfo{}, requestedType, actions, true, &p) { + perms = append(perms, p) + // Shortcut here because this is all we need -- this will turn into all + // scopes. We only need to check for "global" in the direct map. + if _, ok := a.directScopeMap[scope.Global.String()]; !ok { + return perms + } + childScopeMap = nil + scopeMap = map[string][]AclGrant{scope.Global.String(): a.directScopeMap[scope.Global.String()]} + } + + // Next look at children grants; provide only the parent scope ID and tell + // buildPermission to ignore descendants so that we know that the + // permissions being looked at come from a child relationship. Cache the + // scope IDs so we can ignore direct grants. + childrenScopes := map[string]struct{}{} + for scopeId := range childScopeMap { p := Permission{ - ScopeId: scopeId, - Resource: requestedType, - Action: action.ListResolvableAliases, - OnlySelf: true, // default to only self to be restrictive + RoleScopeId: scopeId, + GrantScopeId: globals.GrantScopeChildren, + Resource: requestedType, + Action: action.ListResolvableAliases, + OnlySelf: true, // default to only self to be restrictive + } + if scopeId != scope.Global.String() { // Must be an org then so global is parent + p.RoleParentScopeId = scope.Global.String() + } + if a.buildPermission(&scopes.ScopeInfo{ParentScopeId: scopeId}, requestedType, actions, false, &p) { + perms = append(perms, p) + childrenScopes[scopeId] = struct{}{} + } + } + + // Now look at direct grants; provide only Id so that we know the + // permissions being looked at will include those specific scopes. + for grantScopeId, grants := range scopeMap { + p := Permission{ + GrantScopeId: grantScopeId, + Resource: requestedType, + Action: action.ListResolvableAliases, + OnlySelf: true, // default to only self to be restrictive + } + + if len(grants) > 0 { + // Since scopeIds will be the same for all of these grants, and it's + // not children or descendants, we can get it from any of the grants + p.RoleParentScopeId = grants[0].RoleParentScopeId + p.RoleScopeId = grants[0].RoleScopeId + } + + switch { + case grantScopeId == p.RoleScopeId: + // If the role and grant scope IDs are the same, they share a + // parent, so we can look at the role's parent scope ID in the + // children scopes map + if _, ok := childrenScopes[p.RoleParentScopeId]; ok { + // We already looked at this scope in the children grants, so skip it + continue + } + case strings.HasPrefix(p.RoleScopeId, scope.Org.Prefix()): + // Since direct grants must be in the same scope or downstream, if + // the role scope ID is an org and the role and grant scopes are + // different, the grant is on a project, so look for children from + // the org + if _, ok := childrenScopes[p.RoleScopeId]; ok { + // We already found grants at this scope in the children grants, + // so skip it + continue + } + default: + // Since direct grants must be the same scope or downstream, the + // only possibility left for a children grant is that the parent is + // global and the grant is on the org -- if it was for projects it + // would need to be a descendants grant + if _, ok := childrenScopes[scope.Global.String()]; ok { + // We already looked at this scope in the children grants, so skip it + continue + } } - if a.buildPermission(scopeId, requestedType, actions, &p) { + + if a.buildPermission(&scopes.ScopeInfo{Id: grantScopeId}, requestedType, actions, false, &p) { perms = append(perms, p) } } @@ -307,18 +492,31 @@ func (a ACL) ListResolvablePermissions(requestedType resource.Type, actions acti // or for action.All in order for a Permission to be created for the scope. // The set of "id actions" is resource dependant, but will generally include all // actions that can be taken on an individual resource. -func (a ACL) ListPermissions(requestedScopes map[string]*scopes.ScopeInfo, +func (a ACL) ListPermissions( + requestedScopes map[string]*scopes.ScopeInfo, requestedType resource.Type, idActions action.ActionSet, userId string, ) []Permission { perms := make([]Permission, 0, len(requestedScopes)) - for scopeId := range requestedScopes { + for scopeId, scopeInfo := range requestedScopes { + if scopeInfo == nil { + continue + } + // Note: this function is called either with the scope resulting from + // authentication (which would have the scope info for the specific + // resource) or recursive scopes, which are fully resolved. The scopes + // included have already been run through acl.Allowed() to see if the + // user has access to the resource, so the grant scope ID can correctly + // be set here to be the same as the role scope ID even if it's + // technically coming from children/descendants grants. p := Permission{ - ScopeId: scopeId, - Resource: requestedType, - Action: action.List, - OnlySelf: true, // default to only self to be restrictive + RoleScopeId: scopeId, + RoleParentScopeId: scopeInfo.ParentScopeId, + GrantScopeId: scopeId, + Resource: requestedType, + Action: action.List, + OnlySelf: true, // default to only self to be restrictive } if userId == globals.RecoveryUserId { p.All = true @@ -326,7 +524,7 @@ func (a ACL) ListPermissions(requestedScopes map[string]*scopes.ScopeInfo, perms = append(perms, p) continue } - if a.buildPermission(scopeId, requestedType, idActions, &p) { + if a.buildPermission(scopeInfo, requestedType, idActions, false, &p) { perms = append(perms, p) } } @@ -336,27 +534,43 @@ func (a ACL) ListPermissions(requestedScopes map[string]*scopes.ScopeInfo, // buildPermission populates the provided permission with either the resource ids // or marking All to true if there are grants that have an action that match // one of the provided idActions for the provided type -func (a ACL) buildPermission(scopeId string, +func (a ACL) buildPermission( + scopeInfo *scopes.ScopeInfo, requestedType resource.Type, idActions action.ActionSet, + includeDescendants bool, p *Permission, ) bool { // Get grants for a specific scope id from the source of truth. - grants := a.scopeMap[scopeId] + if scopeInfo == nil { + return false + } + var grants []AclGrant + if scopeInfo.Id != "" { + grants = a.directScopeMap[scopeInfo.Id] + } + if scopeInfo.ParentScopeId != "" { + grants = append(grants, a.childrenScopeMap[scopeInfo.ParentScopeId]...) + } + // If the scope is global it needs to be a direct grant; descendants doesn't + // include global + if includeDescendants || (scopeInfo.Id != "" && scopeInfo.Id != scope.Global.String()) { + grants = append(grants, a.descendantsGrants...) + } for _, grant := range grants { // This grant doesn't match what we're looking for, ignore. - if grant.typ != requestedType && grant.typ != resource.All && globals.ResourceInfoFromPrefix(grant.id).Type != requestedType { + if grant.Type != requestedType && grant.Type != resource.All && globals.ResourceInfoFromPrefix(grant.Id).Type != requestedType { continue } // We found a grant that matches the requested resource type: // Search to see if one or all actions in the action set have been granted. found := false - if ok := grant.actions[action.All]; ok { + if ok := grant.ActionSet[action.All]; ok { found = true } else { for idA := range idActions { - if ok := grant.actions[idA]; ok { + if ok := grant.ActionSet[idA]; ok { found = true break } @@ -375,13 +589,13 @@ func (a ACL) buildPermission(scopeId string, } p.OnlySelf = p.OnlySelf && excludeList.OnlySelf() - switch grant.id { + switch grant.Id { case "*": p.All = true case "": continue default: - p.ResourceIds = append(p.ResourceIds, grant.id) + p.ResourceIds = append(p.ResourceIds, grant.Id) } } diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index 12d3423f3b..02cebf3cd9 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -19,8 +19,10 @@ import ( ) type scopeGrant struct { - scope string - grants []string + roleScope string + roleParentScopeId string + grantScope string + grants []string } func Test_ACLAllowed(t *testing.T) { @@ -45,7 +47,8 @@ func Test_ACLAllowed(t *testing.T) { // A set of common grants to use in the following tests commonGrants := []scopeGrant{ { - scope: "o_a", + roleScope: "o_a", + grantScope: "o_a", grants: []string{ "ids=ampw_bar,ampw_baz;actions=read,update", "ids=ampw_bop;actions=read:self,update", @@ -55,7 +58,8 @@ func Test_ACLAllowed(t *testing.T) { }, }, { - scope: "o_b", + roleScope: "o_b", + grantScope: "o_b", grants: []string{ "ids=*;type=host-set;actions=list,create", "ids=hcst_mypin;type=host;actions=*;output_fields=name,description", @@ -64,7 +68,8 @@ func Test_ACLAllowed(t *testing.T) { }, }, { - scope: "o_d", + roleScope: "o_d", + grantScope: "o_d", grants: []string{ "ids=*;type=*;actions=create,update", "ids=*;type=session;actions=*", @@ -74,7 +79,8 @@ func Test_ACLAllowed(t *testing.T) { } templateGrants := []scopeGrant{ { - scope: "o_c", + roleScope: "o_c", + grantScope: "o_c", grants: []string{ "ids={{user.id }};actions=read,update", "ids={{ account.id}};actions=change-password", @@ -104,7 +110,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "top level create with type only", - resource: Resource{ScopeId: "o_a", Type: resource.HostCatalog}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Type: resource.HostCatalog}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Create, authorized: true}, @@ -113,7 +119,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and id no matching action", - resource: Resource{ScopeId: "o_a", Id: "a_foo", Type: resource.Role}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "a_foo", Type: resource.Role}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Update}, @@ -122,7 +128,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and id and matching action first id", - resource: Resource{ScopeId: "o_a", Id: "ampw_bar"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "ampw_bar"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, authorized: true}, @@ -132,7 +138,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and id and matching action second id", - resource: Resource{ScopeId: "o_a", Id: "ampw_baz"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "ampw_baz"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, authorized: true}, @@ -142,7 +148,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and type and all action with valid pin", - resource: Resource{ScopeId: "o_b", Pin: "hcst_mypin", Type: resource.Host}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_b", Pin: "hcst_mypin", Type: resource.Host}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, authorized: true, outputFields: []string{"description", "id", "name"}}, @@ -152,7 +158,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and type and all action but bad pin", - resource: Resource{ScopeId: "o_b", Pin: "notmypin", Type: resource.Host}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_b", Pin: "notmypin", Type: resource.Host}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, outputFields: []string{"id"}}, @@ -162,7 +168,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and id and some action", - resource: Resource{ScopeId: "o_b", Id: "myhost", Type: resource.HostSet}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_b", Id: "myhost", Type: resource.HostSet}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.List, authorized: true, outputFields: []string{"id"}}, @@ -172,7 +178,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and id and all action but bad specifier", - resource: Resource{ScopeId: "o_b", Id: "id_g"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_b", Id: "id_g"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, outputFields: []string{"id"}}, @@ -182,7 +188,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and not matching type", - resource: Resource{ScopeId: "o_a", Type: resource.HostCatalog}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Type: resource.HostCatalog}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Update}, @@ -191,7 +197,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope and matching type", - resource: Resource{ScopeId: "o_a", Type: resource.HostSet}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Type: resource.HostSet}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.List, authorized: true}, @@ -201,7 +207,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope, type, action, random id and bad pin first id", - resource: Resource{ScopeId: "o_a", Id: "anything", Type: resource.HostCatalog, Pin: "ampw_bar"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "anything", Type: resource.HostCatalog, Pin: "ampw_bar"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Update}, @@ -211,7 +217,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "matching scope, type, action, random id and bad pin second id", - resource: Resource{ScopeId: "o_a", Id: "anything", Type: resource.HostCatalog, Pin: "ampw_baz"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "anything", Type: resource.HostCatalog, Pin: "ampw_baz"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Update}, @@ -221,7 +227,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "wrong scope and matching type", - resource: Resource{ScopeId: "o_bad", Type: resource.HostSet}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_bad", Type: resource.HostSet}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.List}, @@ -231,7 +237,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "any id", - resource: Resource{ScopeId: "o_b", Type: resource.AuthMethod}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_b", Type: resource.AuthMethod}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.List, outputFields: []string{"id"}}, @@ -241,7 +247,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "bad templated user id", - resource: Resource{ScopeId: "o_c"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_c"}, scopeGrants: append(commonGrants, templateGrants...), actionsAuthorized: []actionAuthorized{ {action: action.List}, @@ -252,7 +258,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "good templated user id", - resource: Resource{ScopeId: "o_c", Id: "u_abcd1234"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_c", Id: "u_abcd1234"}, scopeGrants: append(commonGrants, templateGrants...), actionsAuthorized: []actionAuthorized{ {action: action.Read, authorized: true}, @@ -262,7 +268,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "bad templated old account id", - resource: Resource{ScopeId: "o_c"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_c"}, scopeGrants: append(commonGrants, templateGrants...), actionsAuthorized: []actionAuthorized{ {action: action.List}, @@ -273,7 +279,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "good templated old account id", - resource: Resource{ScopeId: "o_c", Id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix)}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_c", Id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix)}, scopeGrants: append(commonGrants, templateGrants...), actionsAuthorized: []actionAuthorized{ {action: action.ChangePassword, authorized: true}, @@ -283,7 +289,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "bad templated new account id", - resource: Resource{ScopeId: "o_c"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_c"}, scopeGrants: append(commonGrants, templateGrants...), actionsAuthorized: []actionAuthorized{ {action: action.List}, @@ -294,7 +300,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "good templated new account id", - resource: Resource{ScopeId: "o_c", Id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix)}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_c", Id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix)}, scopeGrants: append(commonGrants, templateGrants...), actionsAuthorized: []actionAuthorized{ {action: action.ChangePassword, authorized: true}, @@ -304,7 +310,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "all type", - resource: Resource{ScopeId: "o_d", Type: resource.Account}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_d", Type: resource.Account}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Create, authorized: true}, @@ -314,7 +320,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "list with top level list", - resource: Resource{ScopeId: "o_a", Type: resource.Target}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Type: resource.Target}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.List, authorized: true}, @@ -322,7 +328,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "list sessions with wildcard actions", - resource: Resource{ScopeId: "o_d", Type: resource.Session}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_d", Type: resource.Session}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.List, authorized: true}, @@ -330,7 +336,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "read self with top level read first id", - resource: Resource{ScopeId: "o_a", Id: "ampw_bar"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "ampw_bar"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, authorized: true}, @@ -339,7 +345,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "read self with top level read second id", - resource: Resource{ScopeId: "o_a", Id: "ampw_baz"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "ampw_baz"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read, authorized: true}, @@ -348,7 +354,7 @@ func Test_ACLAllowed(t *testing.T) { }, { name: "read self only", - resource: Resource{ScopeId: "o_a", Id: "ampw_bop"}, + resource: Resource{ParentScopeId: scope.Global.String(), ScopeId: "o_a", Id: "ampw_bop"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read}, @@ -360,7 +366,8 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: scope.Global.String(), Type: resource.Worker}, scopeGrants: []scopeGrant{ { - scope: scope.Global.String(), + roleScope: scope.Global.String(), + grantScope: scope.Global.String(), grants: []string{ "type=worker;actions=create", }, @@ -375,7 +382,8 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: scope.Global.String(), Type: resource.Worker}, scopeGrants: []scopeGrant{ { - scope: scope.Global.String(), + roleScope: scope.Global.String(), + grantScope: scope.Global.String(), grants: []string{ "type=worker;actions=create:worker-led", }, @@ -392,7 +400,7 @@ func Test_ACLAllowed(t *testing.T) { var grants []Grant for _, sg := range test.scopeGrants { for _, g := range sg.grants { - grant, err := Parse(ctx, sg.scope, g, WithAccountId(test.accountId), WithUserId(test.userId)) + grant, err := Parse(ctx, GrantTuple{RoleScopeId: sg.roleScope, GrantScopeId: sg.grantScope, Grant: g}, WithAccountId(test.accountId), WithUserId(test.userId)) require.NoError(t, err) grants = append(grants, grant) } @@ -429,8 +437,10 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Requested resource mismatch", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=target;actions=list,read"}, // List & Read for all Targets + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=target;actions=list,read"}, // List & Read for all Targets }, }, resourceType: resource.Session, // We're requesting sessions. @@ -441,8 +451,10 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Requested actions not available for the requested scope id", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=delete"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=delete"}, }, }, resourceType: resource.Session, @@ -453,8 +465,10 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "No specific id or wildcard provided for `id` field", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"type=*;actions=list,read"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"type=*;actions=list,read"}, }, }, resourceType: resource.Session, @@ -466,20 +480,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Allow all ids", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=update,read"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=update,read"}, }, }, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -487,20 +505,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Allow all ids, :self actions", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,read:self"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,read:self"}, }, }, resourceType: resource.Session, actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -508,7 +530,9 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Allow specific IDs", aclGrants: []scopeGrant{ { - scope: "o_1", + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", grants: []string{ "ids=s_1;type=session;actions=list,read", "ids=s_2,s_3;type=session;actions=list,read", @@ -519,12 +543,14 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: []string{"s_1", "s_2", "s_3"}, - OnlySelf: false, - All: false, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, }, }, }, @@ -532,20 +558,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "No specific type 1", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=list,read:self"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=list,read:self"}, }, }, resourceType: resource.Session, actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -553,20 +583,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "List + No-op action with id wildcard", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,no-op"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,no-op"}, }, }, resourceType: resource.Session, actionSet: action.NewActionSet(action.NoOp), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -574,8 +608,10 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "List + No-op action with id wildcard, read present", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,no-op"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,no-op"}, }, }, resourceType: resource.Session, @@ -586,7 +622,9 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "List + No-op action with specific ids", aclGrants: []scopeGrant{ { - scope: "o_1", + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", grants: []string{ "ids=s_1;type=session;actions=list,no-op", "ids=s_2,s_3;type=session;actions=list,no-op", @@ -597,12 +635,14 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { actionSet: action.NewActionSet(action.NoOp), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: []string{"s_1", "s_2", "s_3"}, - OnlySelf: false, - All: false, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, }, }, }, @@ -610,20 +650,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "No specific type 2", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=list,read:self"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=list,read:self"}, }, }, resourceType: resource.Host, actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Host, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Host, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -631,7 +675,9 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Grant hierarchy is respected", aclGrants: []scopeGrant{ { - scope: "o_1", + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", grants: []string{ "ids=*;type=*;actions=*", "ids=*;type=session;actions=cancel:self,list,read:self", @@ -642,12 +688,14 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { actionSet: action.NewActionSet(action.NoOp, action.Read, action.ReadSelf, action.Cancel, action.CancelSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -655,20 +703,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Full access 1", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=*"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=*"}, }, }, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -676,20 +728,24 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Full access 2", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=*"}, + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=*"}, }, }, resourceType: resource.Host, actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Host, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Host, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -697,35 +753,43 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { name: "Multiple scopes", aclGrants: []scopeGrant{ { - scope: "o_1", + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", grants: []string{ "ids=s_1;type=session;actions=create,read", "ids=s_2,s_3;type=session;actions=update,read", }, }, { - scope: "o_2", - grants: []string{"ids=*;type=session;actions=read:self"}, + roleScope: "o_2", + grantScope: "o_2", + roleParentScopeId: scope.Global.String(), + grants: []string{"ids=*;type=session;actions=read:self"}, }, }, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read, action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: []string{"s_1", "s_2", "s_3"}, - OnlySelf: false, - All: false, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, }, { - ScopeId: "o_2", - Resource: resource.Session, - Action: action.ListResolvableAliases, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_2", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_2", + Resource: resource.Session, + Action: action.ListResolvableAliases, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -735,7 +799,9 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { actionSet: action.NewActionSet(action.Read, action.Cancel), aclGrants: []scopeGrant{ { - scope: "p_1", + roleScope: "p_1", + roleParentScopeId: "o_1", + grantScope: "p_1", grants: []string{ "type=target;actions=list", "ids=ttcp_1234567890;actions=read", @@ -744,12 +810,287 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { }, expPermissions: []Permission{ { - ScopeId: "p_1", - Resource: resource.Target, - Action: action.ListResolvableAliases, - ResourceIds: []string{"ttcp_1234567890"}, - All: false, - OnlySelf: false, + RoleScopeId: "p_1", + RoleParentScopeId: "o_1", + GrantScopeId: "p_1", + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "global_no_this_with_descendants", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "global", + grantScope: globals.GrantScopeDescendants, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "global_with_this_with_descendants", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "global", + grantScope: globals.GrantScopeDescendants, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "global", + grantScope: "global", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: scope.Global.String(), + GrantScopeId: scope.Global.String(), + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "global_no_this_with_valid_children", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "global", + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "org_no_this_with_children_and_direct_grant", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "o_2", + roleParentScopeId: scope.Global.String(), + grantScope: "o_2", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "o_2", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_2", + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "org_with_this_with_children_and_direct_grant", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "org_with_this_with_child_scope_direct_grants", + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "o_1", + roleParentScopeId: scope.Global.String(), + grantScope: "o_1", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "p_1a", + roleParentScopeId: "o_1", + grantScope: "p_1a", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "p_1b", + roleParentScopeId: "o_1", + grantScope: "p_1b", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "p_2", + roleParentScopeId: "o_2", + grantScope: "p_2", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "p_2", + RoleParentScopeId: "o_2", + GrantScopeId: "p_2", + Resource: resource.Target, + Action: action.ListResolvableAliases, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, }, }, }, @@ -759,15 +1100,18 @@ func TestACL_ListResolvableAliasesPermissions(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var grants []Grant for _, sg := range tt.aclGrants { + if sg.roleScope == "" { + sg.roleScope = sg.grantScope + } for _, g := range sg.grants { - grant, err := Parse(ctx, sg.scope, g, WithSkipFinalValidation(tt.skipGrantValidationChecking)) + grant, err := Parse(ctx, GrantTuple{RoleScopeId: sg.roleScope, RoleParentScopeId: sg.roleParentScopeId, GrantScopeId: sg.grantScope, Grant: g}, WithSkipFinalValidation(tt.skipGrantValidationChecking)) require.NoError(t, err) grants = append(grants, grant) } } acl := NewACL(grants...) - perms := acl.ListResolvablePermissions(tt.resourceType, tt.actionSet) + perms := acl.ListResolvableAliasesPermissions(tt.resourceType, tt.actionSet) require.ElementsMatch(t, tt.expPermissions, perms) }) } @@ -792,8 +1136,8 @@ func TestACL_ListPermissions(t *testing.T) { name: "Requested scope(s) not present in ACL scope map", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,read"}, + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,read"}, }, }, scopes: map[string]*scopes.ScopeInfo{ @@ -808,8 +1152,8 @@ func TestACL_ListPermissions(t *testing.T) { name: "Requested resource mismatch", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=target;actions=list,read"}, // List & Read for all Targets + grantScope: "o_1", + grants: []string{"ids=*;type=target;actions=list,read"}, // List & Read for all Targets }, }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, @@ -821,8 +1165,8 @@ func TestACL_ListPermissions(t *testing.T) { name: "Requested actions not available for the requested scope id", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=delete"}, + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=delete"}, }, }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, @@ -834,8 +1178,8 @@ func TestACL_ListPermissions(t *testing.T) { name: "No specific id or wildcard provided for `id` field", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"type=*;actions=list,read"}, + grantScope: "o_1", + grants: []string{"type=*;actions=list,read"}, }, }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, @@ -848,21 +1192,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "Allow all ids", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,read"}, + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,read"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -870,21 +1216,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "Allow all ids, :self actions", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,read:self"}, + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,read:self"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -892,24 +1240,26 @@ func TestACL_ListPermissions(t *testing.T) { name: "Allow specific IDs", aclGrants: []scopeGrant{ { - scope: "o_1", + grantScope: "o_1", grants: []string{ "ids=s_1;type=session;actions=list,read", "ids=s_2,s_3;type=session;actions=list,read", }, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: []string{"s_1", "s_2", "s_3"}, - OnlySelf: false, - All: false, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, }, }, }, @@ -917,21 +1267,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "No specific type 1", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=list,read:self"}, + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=list,read:self"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -939,21 +1291,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "List + No-op action with id wildcard", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,no-op"}, + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,no-op"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.NoOp), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -961,11 +1315,11 @@ func TestACL_ListPermissions(t *testing.T) { name: "List + No-op action with id wildcard, read present", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=session;actions=list,no-op"}, + grantScope: "o_1", + grants: []string{"ids=*;type=session;actions=list,no-op"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{}, @@ -974,24 +1328,26 @@ func TestACL_ListPermissions(t *testing.T) { name: "List + No-op action with specific ids", aclGrants: []scopeGrant{ { - scope: "o_1", + grantScope: "o_1", grants: []string{ "ids=s_1;type=session;actions=list,no-op", "ids=s_2,s_3;type=session;actions=list,no-op", }, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.NoOp), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: []string{"s_1", "s_2", "s_3"}, - OnlySelf: false, - All: false, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, }, }, }, @@ -999,21 +1355,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "No specific type 2", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=list,read:self"}, + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=list,read:self"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Host, actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Host, - Action: action.List, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Host, + Action: action.List, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, @@ -1021,24 +1379,26 @@ func TestACL_ListPermissions(t *testing.T) { name: "Grant hierarchy is respected", aclGrants: []scopeGrant{ { - scope: "o_1", + grantScope: "o_1", grants: []string{ "ids=*;type=*;actions=*", "ids=*;type=session;actions=cancel:self,list,read:self", }, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.NoOp, action.Read, action.ReadSelf, action.Cancel, action.CancelSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -1046,21 +1406,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "Full access 1", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=*"}, + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=*"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -1068,21 +1430,23 @@ func TestACL_ListPermissions(t *testing.T) { name: "Full access 2", aclGrants: []scopeGrant{ { - scope: "o_1", - grants: []string{"ids=*;type=*;actions=*"}, + grantScope: "o_1", + grants: []string{"ids=*;type=*;actions=*"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}}, resourceType: resource.Host, actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Host, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Host, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, @@ -1090,97 +1454,137 @@ func TestACL_ListPermissions(t *testing.T) { name: "Multiple scopes", aclGrants: []scopeGrant{ { - scope: "o_1", + grantScope: "o_1", grants: []string{ "ids=s_1;type=session;actions=list,read", "ids=s_2,s_3;type=session;actions=list,read", }, }, { - scope: "o_2", - grants: []string{"ids=*;type=session;actions=list,read:self"}, + grantScope: "o_2", + grants: []string{"ids=*;type=session;actions=list,read:self"}, }, }, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil, "o_2": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}, "o_2": {Id: "o_2", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read, action.ReadSelf), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: []string{"s_1", "s_2", "s_3"}, - OnlySelf: false, - All: false, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: []string{"s_1", "s_2", "s_3"}, + OnlySelf: false, + All: false, }, { - ScopeId: "o_2", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: true, - All: true, + RoleScopeId: "o_2", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_2", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: true, + All: true, }, }, }, { name: "Allow recovery user full access to sessions", userId: globals.RecoveryUserId, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil, "o_2": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}, "o_2": {Id: "o_2", ParentScopeId: scope.Global.String()}}, resourceType: resource.Session, actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, { - ScopeId: "o_2", - Resource: resource.Session, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_2", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_2", + Resource: resource.Session, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, { name: "Allow recovery user full access to targets", userId: globals.RecoveryUserId, - scopes: map[string]*scopes.ScopeInfo{"o_1": nil, "o_2": nil}, + scopes: map[string]*scopes.ScopeInfo{"o_1": {Id: "o_1", ParentScopeId: scope.Global.String()}, "o_2": {Id: "o_2", ParentScopeId: scope.Global.String()}}, resourceType: resource.Target, actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { - ScopeId: "o_1", - Resource: resource.Target, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_1", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, { - ScopeId: "o_2", - Resource: resource.Target, - Action: action.List, - ResourceIds: nil, - OnlySelf: false, - All: true, + RoleScopeId: "o_2", + RoleParentScopeId: scope.Global.String(), + GrantScopeId: "o_2", + Resource: resource.Target, + Action: action.List, + ResourceIds: nil, + OnlySelf: false, + All: true, }, }, }, { name: "separate_type_id_resource_grants", - scopes: map[string]*scopes.ScopeInfo{"p_1": nil}, + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}}, + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + grantScope: "p_1", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "p_1", + RoleParentScopeId: "o_1", + GrantScopeId: "p_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "global_no_this_with_descendants", + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}, "global": {Id: "global", ParentScopeId: ""}}, resourceType: resource.Target, actionSet: action.NewActionSet(action.Read, action.Cancel), aclGrants: []scopeGrant{ { - scope: "p_1", + roleScope: "global", + grantScope: globals.GrantScopeDescendants, grants: []string{ "type=target;actions=list", "ids=ttcp_1234567890;actions=read", @@ -1189,12 +1593,196 @@ func TestACL_ListPermissions(t *testing.T) { }, expPermissions: []Permission{ { - ScopeId: "p_1", - Resource: resource.Target, - Action: action.List, - ResourceIds: []string{"ttcp_1234567890"}, - All: false, - OnlySelf: false, + RoleScopeId: "p_1", + RoleParentScopeId: "o_1", + GrantScopeId: "p_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "global_with_this_with_descendants", + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}, "global": {Id: "global", ParentScopeId: ""}}, + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "global", + grantScope: globals.GrantScopeDescendants, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "global", + grantScope: "global", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "p_1", + RoleParentScopeId: "o_1", + GrantScopeId: "p_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "global", + GrantScopeId: "global", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "global_no_this_with_invalid_children", + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}, "global": {Id: "global", ParentScopeId: ""}}, + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "global", + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: nil, + }, + { + name: "global_no_this_with_valid_children", + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}, "o_2": {Id: "o_2", ParentScopeId: "global"}}, + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "global", + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "o_2", + RoleParentScopeId: "global", + GrantScopeId: "o_2", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "org_no_this_with_children_and_direct_grant", + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}, "o_2": {Id: "o_2", ParentScopeId: "global"}}, + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "o_1", + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "o_2", + grantScope: "o_2", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "p_1", + RoleParentScopeId: "o_1", + GrantScopeId: "p_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "o_2", + RoleParentScopeId: "global", + GrantScopeId: "o_2", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + }, + }, + { + name: "org_with_this_with_children_and_direct_grant", + scopes: map[string]*scopes.ScopeInfo{"p_1": {Id: "p_1", ParentScopeId: "o_1"}, "o_2": {Id: "o_2", ParentScopeId: "global"}, "o_1": {Id: "o_1", ParentScopeId: "global"}}, + resourceType: resource.Target, + actionSet: action.NewActionSet(action.Read, action.Cancel), + aclGrants: []scopeGrant{ + { + roleScope: "o_1", + grantScope: globals.GrantScopeChildren, + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + { + roleScope: "o_1", + grantScope: "o_1", + grants: []string{ + "type=target;actions=list", + "ids=ttcp_1234567890;actions=read", + }, + }, + }, + expPermissions: []Permission{ + { + RoleScopeId: "o_1", + RoleParentScopeId: "global", + GrantScopeId: "o_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, + }, + { + RoleScopeId: "p_1", + RoleParentScopeId: "o_1", + GrantScopeId: "p_1", + Resource: resource.Target, + Action: action.List, + ResourceIds: []string{"ttcp_1234567890"}, + All: false, + OnlySelf: false, }, }, }, @@ -1208,8 +1796,11 @@ func TestACL_ListPermissions(t *testing.T) { } var grants []Grant for _, sg := range tt.aclGrants { + if sg.roleScope == "" { + sg.roleScope = sg.grantScope + } for _, g := range sg.grants { - grant, err := Parse(ctx, sg.scope, g, WithSkipFinalValidation(tt.skipGrantValidationChecking)) + grant, err := Parse(ctx, GrantTuple{RoleScopeId: sg.roleScope, GrantScopeId: sg.grantScope, Grant: g}, WithSkipFinalValidation(tt.skipGrantValidationChecking)) require.NoError(t, err) grants = append(grants, grant) } @@ -1315,7 +1906,7 @@ func Test_AnonRestrictions(t *testing.T) { grant = fmt.Sprintf(grant, action.Type(j).String()) } - parsedGrant, err := Parse(ctx, scope.Global.String(), grant, WithSkipFinalValidation(true)) + parsedGrant, err := Parse(ctx, GrantTuple{RoleScopeId: scope.Global.String(), GrantScopeId: scope.Global.String(), Grant: grant}, WithSkipFinalValidation(true)) require.NoError(err) acl := NewACL(parsedGrant) diff --git a/internal/perms/grants.go b/internal/perms/grants.go index fa8a061356..97d5bc8948 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -22,11 +22,11 @@ import ( "golang.org/x/exp/slices" ) -type actionSet map[action.Type]bool +type ActionSet map[action.Type]bool // Actions is a helper that goes through the map and returns both the actual // types of actions as a slice and the equivalent strings -func (a actionSet) Actions() (typs []action.Type, strs []string) { +func (a ActionSet) Actions() (typs []action.Type, strs []string) { typs = make([]action.Type, 0, len(a)) strs = make([]string, 0, len(a)) for k, v := range a { @@ -43,9 +43,11 @@ func (a actionSet) Actions() (typs []action.Type, strs []string) { // GrantTuple is simply a struct that can be reference from other code to return // a set of scopes and grants to parse type GrantTuple struct { - RoleId string - ScopeId string - Grant string + RoleId string + RoleScopeId string + RoleParentScopeId string + GrantScopeId string + Grant string } type GrantTuples []GrantTuple @@ -56,7 +58,7 @@ func (g GrantTuples) GrantHash(ctx context.Context) ([]byte, error) { // TODO: Should this return an error when the GrantTuples is empty? var values []string for _, grant := range g { - values = append(values, grant.Grant, grant.RoleId, grant.ScopeId) + values = append(values, grant.Grant, grant.RoleId, grant.GrantScopeId) } // Sort for deterministic output slices.Sort(values) @@ -106,14 +108,17 @@ type Scope struct { // Id is the public id of the iam.Scope Id string - // Type is the scope's type (org or project) - Type scope.Type + // ParentId is the parent scope ID + ParentId string } // Grant is a Go representation of a parsed grant type Grant struct { - // The scope, containing the ID and type - scope Scope + // The role scope ID + roleScopeId string + + // The role's parent scope ID, if any + roleParentScopeId string // The ID of the grant, if provided. Deprecated in favor of ids. id string @@ -121,11 +126,14 @@ type Grant struct { // The IDs in the grant, if provided ids []string + // The grant scope ID of the grant + grantScopeId string + // The type, if provided typ resource.Type // The set of actions being granted - actions actionSet + actions ActionSet // The set of output fields granted OutputFields *OutputFields @@ -145,6 +153,11 @@ func (g Grant) Ids() []string { return g.ids } +// GrantScopeId returns the grant scope ID the grant refers to, if any +func (g Grant) GrantScopeId() string { + return g.grantScopeId +} + // Type returns the type the grant refers to, or Unknown func (g Grant) Type() resource.Type { return g.typ @@ -172,10 +185,12 @@ func (g Grant) hasActionOrSubaction(act action.Type) bool { func (g Grant) clone() *Grant { ret := &Grant{ - scope: g.scope, - id: g.id, - ids: g.ids, - typ: g.typ, + roleScopeId: g.roleScopeId, + roleParentScopeId: g.roleParentScopeId, + id: g.id, + ids: g.ids, + grantScopeId: g.grantScopeId, + typ: g.typ, } if g.ids != nil { ret.ids = make([]string, len(g.ids)) @@ -435,47 +450,42 @@ func (g *Grant) unmarshalText(ctx context.Context, grantString string) error { // // The scope must be the org and project where this grant originated, not the // request. -func Parse(ctx context.Context, scopeId, grantString string, opt ...Option) (Grant, error) { +func Parse(ctx context.Context, tuple GrantTuple, opt ...Option) (Grant, error) { const op = "perms.Parse" - if len(grantString) == 0 { + if len(tuple.Grant) == 0 { return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "missing grant string") } - if scopeId == "" { - return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + if tuple.RoleScopeId == "" { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "missing role scope id") + } + if tuple.GrantScopeId == "" { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "missing grant scope id") } - grantString = strings.ToValidUTF8(grantString, string(unicode.ReplacementChar)) + tuple.Grant = strings.ToValidUTF8(tuple.Grant, string(unicode.ReplacementChar)) grant := Grant{ - scope: Scope{Id: strings.ToValidUTF8(scopeId, string(unicode.ReplacementChar))}, - } - switch { - case scopeId == scope.Global.String(): - grant.scope.Type = scope.Global - case strings.HasPrefix(scopeId, scope.Org.Prefix()): - grant.scope.Type = scope.Org - case strings.HasPrefix(scopeId, scope.Project.Prefix()): - grant.scope.Type = scope.Project - default: - return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "invalid scope type") + roleScopeId: strings.ToValidUTF8(tuple.RoleScopeId, string(unicode.ReplacementChar)), + roleParentScopeId: tuple.RoleParentScopeId, + grantScopeId: tuple.GrantScopeId, } switch { - case grantString[0] == '{': - if err := grant.unmarshalJSON(ctx, []byte(grantString)); err != nil { + case tuple.Grant[0] == '{': + if err := grant.unmarshalJSON(ctx, []byte(tuple.Grant)); err != nil { return Grant{}, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse JSON grant string")) } default: - if err := grant.unmarshalText(ctx, grantString); err != nil { + if err := grant.unmarshalText(ctx, tuple.Grant); err != nil { return Grant{}, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse grant string")) } } if grant.id != "" && len(grant.ids) > 0 { - return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains both %q and %q fields", grantString, "id", "ids")) + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains both %q and %q fields", tuple.Grant, "id", "ids")) } if len(grant.ids) > 1 && slices.Contains(grant.ids, "*") { - return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains both wildcard and non-wildcard values in %q field", grantString, "ids")) + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains both wildcard and non-wildcard values in %q field", tuple.Grant, "ids")) } opts := getOpts(opt...) @@ -498,7 +508,7 @@ func Parse(ctx context.Context, scopeId, grantString string, opt ...Option) (Gra continue } if seenType != globals.ResourceInfoFromPrefix(id).Type { - return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains ids of differently-typed resources", grantString)) + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains ids of differently-typed resources", tuple.Grant)) } } } @@ -646,19 +656,43 @@ func Parse(ctx context.Context, scopeId, grantString string, opt ...Option) (Gra grantForValidation := grant.clone() grantForValidation.id = grantIds[i] acl := NewACL(*grantForValidation) - r := Resource{ - ScopeId: scopeId, - Id: grantIds[i], - Type: grant.typ, - } - if !resource.TopLevelType(grant.typ) { - r.Pin = grantIds[i] + // For special scope names we aren't sure where the resource + // might be, so check possible scopes and see if any are valid + scopesToCheck := make([]string, 0, 2) + var parentScopeId string + switch { + case grant.grantScopeId == globals.GrantScopeDescendants: + scopesToCheck = append(scopesToCheck, "o_1234567890", "p_1234567890") + case grant.grantScopeId == globals.GrantScopeChildren: + if grant.roleScopeId == scope.Global.String() { + scopesToCheck = append(scopesToCheck, "o_1234567890") + parentScopeId = scope.Global.String() + } else { + scopesToCheck = append(scopesToCheck, "p_1234567890") + parentScopeId = grant.roleScopeId + } + default: + scopesToCheck = append(scopesToCheck, grant.grantScopeId) } var allowed bool - for k := range grant.actions { - results := acl.Allowed(r, k, globals.AnonymousUserId, WithSkipAnonymousUserRestrictions(true)) - if results.Authorized { - allowed = true + for _, scopeId := range scopesToCheck { + r := Resource{ + ScopeId: scopeId, + Id: grantIds[i], + Type: grant.typ, + ParentScopeId: parentScopeId, + } + if !resource.TopLevelType(grant.typ) { + r.Pin = grantIds[i] + } + for k := range grant.actions { + results := acl.Allowed(r, k, globals.AnonymousUserId, WithSkipAnonymousUserRestrictions(true)) + if results.Authorized { + allowed = true + break + } + } + if allowed { break } } diff --git a/internal/perms/grants_test.go b/internal/perms/grants_test.go index 071b29f989..f2bcd68804 100644 --- a/internal/perms/grants_test.go +++ b/internal/perms/grants_test.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" - "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -141,22 +140,14 @@ func Test_MarshalingAndCloning(t *testing.T) { tests := []input{ { - name: "empty", - input: Grant{ - scope: Scope{ - Type: scope.Org, - }, - }, + name: "empty", jsonOutput: `{}`, canonicalString: ``, }, { name: "type and id", input: Grant{ - id: "baz", - scope: Scope{ - Type: scope.Project, - }, + id: "baz", typ: resource.Group, }, jsonOutput: `{"id":"baz","type":"group"}`, @@ -166,9 +157,6 @@ func Test_MarshalingAndCloning(t *testing.T) { name: "type and ids", input: Grant{ ids: []string{"baz", "bop"}, - scope: Scope{ - Type: scope.Project, - }, typ: resource.Group, }, jsonOutput: `{"ids":["baz","bop"],"type":"group"}`, @@ -178,9 +166,6 @@ func Test_MarshalingAndCloning(t *testing.T) { name: "type and ids single id", input: Grant{ ids: []string{"baz"}, - scope: Scope{ - Type: scope.Project, - }, typ: resource.Group, }, jsonOutput: `{"ids":["baz"],"type":"group"}`, @@ -189,10 +174,7 @@ func Test_MarshalingAndCloning(t *testing.T) { { name: "output fields id", input: Grant{ - id: "baz", - scope: Scope{ - Type: scope.Project, - }, + id: "baz", typ: resource.Group, OutputFields: &OutputFields{ fields: map[string]bool{ @@ -209,9 +191,6 @@ func Test_MarshalingAndCloning(t *testing.T) { name: "output fields ids", input: Grant{ ids: []string{"baz", "bop"}, - scope: Scope{ - Type: scope.Project, - }, typ: resource.Group, OutputFields: &OutputFields{ fields: map[string]bool{ @@ -227,10 +206,7 @@ func Test_MarshalingAndCloning(t *testing.T) { { name: "everything id", input: Grant{ - id: "baz", - scope: Scope{ - Type: scope.Project, - }, + id: "baz", typ: resource.Group, actions: map[action.Type]bool{ action.Create: true, @@ -252,9 +228,6 @@ func Test_MarshalingAndCloning(t *testing.T) { name: "everything ids", input: Grant{ ids: []string{"baz", "bop"}, - scope: Scope{ - Type: scope.Project, - }, typ: resource.Group, actions: map[action.Type]bool{ action.Create: true, @@ -663,160 +636,14 @@ func Test_Parse(t *testing.T) { } tests := []input{ - { - name: "empty", - err: `perms.Parse: missing grant string: parameter violation: error #100`, - }, - { - name: "bad json", - input: "{2:193}", - err: `perms.Parse: unable to parse JSON grant string: perms.(Grant).unmarshalJSON: error occurred during decode, encoding issue: error #303: invalid character '2' looking for beginning of object key string`, - }, - { - name: "bad text", - input: "id=foo=bar", - err: `perms.Parse: unable to parse grant string: perms.(Grant).unmarshalText: segment "id=foo=bar" not formatted correctly, wrong number of equal signs: parameter violation: error #100`, - }, - { - name: "bad type", - input: "ids=s_foobar;type=barfoo;actions=read", - err: `perms.Parse: unable to parse grant string: perms.(Grant).unmarshalText: unknown type specifier "barfoo": parameter violation: error #100`, - }, - { - name: "bad actions", - input: "ids=hcst_foobar;type=host-catalog;actions=createread", - err: `perms.Parse: perms.(Grant).parseAndValidateActions: unknown action "createread": parameter violation: error #100`, - }, - { - name: "bad id type", - input: "id=foobar;actions=read", - err: `perms.Parse: parsed grant string "id=foobar;actions=read" contains an id "foobar" of an unknown resource type: parameter violation: error #100`, - }, - { - name: "bad ids type first position", - input: "ids=foobar,hcst_foobar;actions=read", - err: `perms.Parse: input grant string "ids=foobar,hcst_foobar;actions=read" contains ids of differently-typed resources: parameter violation: error #100`, - }, - { - name: "bad ids type second position", - input: "ids=hcst_foobar,foobar;actions=read", - err: `perms.Parse: input grant string "ids=hcst_foobar,foobar;actions=read" contains ids of differently-typed resources: parameter violation: error #100`, - }, - { - name: "bad create action for ids", - input: "ids=u_foobar;actions=create", - err: `perms.Parse: parsed grant string "ids=u_foobar;actions=create" contains create or list action in a format that does not allow these: parameter violation: error #100`, - }, - { - name: "bad create action for ids with other perms", - input: "ids=u_foobar;actions=read,create", - err: `perms.Parse: parsed grant string "ids=u_foobar;actions=create,read" contains create or list action in a format that does not allow these: parameter violation: error #100`, - }, - { - name: "bad list action for id", - input: "id=u_foobar;actions=list", - err: `perms.Parse: parsed grant string "id=u_foobar;actions=list" contains create or list action in a format that does not allow these: parameter violation: error #100`, - }, - { - name: "bad list action for type with other perms", - input: "type=host-catalog;actions=list,read", - err: `perms.Parse: parsed grant string "type=host-catalog;actions=list,read" contains non-create or non-list action in a format that only allows these: parameter violation: error #100`, - }, - { - name: "wildcard id and actions without collection", - input: "id=*;actions=read", - err: `perms.Parse: parsed grant string "id=*;actions=read" contains wildcard id and no specified type: parameter violation: error #100`, - }, - { - name: "wildcard ids and actions without collection", - input: "ids=*;actions=read", - err: `perms.Parse: parsed grant string "ids=*;actions=read" contains wildcard id and no specified type: parameter violation: error #100`, - }, - { - name: "wildcard id and actions with list", - input: "id=*;actions=read,list", - err: `perms.Parse: parsed grant string "id=*;actions=list,read" contains wildcard id and no specified type: parameter violation: error #100`, - }, - { - name: "wildcard ids and actions with list", - input: "ids=*;actions=read,list", - err: `perms.Parse: parsed grant string "ids=*;actions=list,read" contains wildcard id and no specified type: parameter violation: error #100`, - }, - { - name: "wildcard type with no ids", - input: "type=*;actions=read,list", - err: `perms.Parse: parsed grant string "type=*;actions=list,read" contains wildcard type with no id value: parameter violation: error #100`, - }, - { - name: "mixed wildcard and non wildcard ids first position", - input: "ids=*,u_foobar;actions=read,list", - err: `perms.Parse: input grant string "ids=*,u_foobar;actions=read,list" contains both wildcard and non-wildcard values in "ids" field: parameter violation: error #100`, - }, - { - name: "mixed wildcard and non wildcard ids second position", - input: "ids=u_foobar,*;actions=read,list", - err: `perms.Parse: input grant string "ids=u_foobar,*;actions=read,list" contains both wildcard and non-wildcard values in "ids" field: parameter violation: error #100`, - }, - { - name: "empty ids and type", - input: "actions=create", - err: `perms.Parse: parsed grant string "actions=create" contains no id or type: parameter violation: error #100`, - }, - { - name: "wildcard type non child id", - input: "id=ttcp_1234567890;type=*;actions=create", - err: `perms.Parse: parsed grant string "id=ttcp_1234567890;type=*;actions=create" contains an id that does not support child types: parameter violation: error #100`, - }, - { - name: "wildcard type non child ids first position", - input: "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create", - err: `perms.Parse: parsed grant string "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create" contains an id that does not support child types: parameter violation: error #100`, - }, - { - name: "wildcard type non child ids second position", - input: "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create", - err: `perms.Parse: parsed grant string "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create" contains an id that does not support child types: parameter violation: error #100`, - }, - { - name: "specified resource type non child id", - input: "id=hcst_1234567890;type=account;actions=read", - err: `perms.Parse: parsed grant string "id=hcst_1234567890;type=account;actions=read" contains type account that is not a child type of the type (host-catalog) of the specified id: parameter violation: error #100`, - }, - { - name: "specified resource type non child ids first position", - input: "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read", - err: `perms.Parse: parsed grant string "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read" contains type account that is not a child type of the type (host-catalog) of the specified id: parameter violation: error #100`, - }, - { - name: "specified resource type non child ids second position", - input: "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read", - err: `perms.Parse: parsed grant string "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read" contains type account that is not a child type of the type (host-catalog) of the specified id: parameter violation: error #100`, - }, - { - name: "no id with one bad action", - input: "type=host-set;actions=read", - err: `perms.Parse: parsed grant string "type=host-set;actions=read" contains non-create or non-list action in a format that only allows these: parameter violation: error #100`, - }, - { - name: "no id with two bad action", - input: "type=host-set;actions=read,create", - err: `perms.Parse: parsed grant string "type=host-set;actions=create,read" contains non-create or non-list action in a format that only allows these: parameter violation: error #100`, - }, - { - name: "no id with three bad action", - input: "type=host-set;actions=list,read,create", - err: `perms.Parse: parsed grant string "type=host-set;actions=create,list,read" contains non-create or non-list action in a format that only allows these: parameter violation: error #100`, - }, { name: "empty output fields", input: "id=*;type=*;actions=read,list;output_fields=", expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "*", - typ: resource.All, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "*", + typ: resource.All, actions: map[action.Type]bool{ action.Read: true, action.List: true, @@ -830,12 +657,10 @@ func Test_Parse(t *testing.T) { name: "empty output fields json", input: `{"id": "*", "type": "*", "actions": ["read", "list"], "output_fields": []}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "*", - typ: resource.All, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "*", + typ: resource.All, actions: map[action.Type]bool{ action.Read: true, action.List: true, @@ -849,12 +674,10 @@ func Test_Parse(t *testing.T) { name: "wildcard id and type and actions with list", input: "id=*;type=*;actions=read,list", expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "*", - typ: resource.All, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "*", + typ: resource.All, actions: map[action.Type]bool{ action.Read: true, action.List: true, @@ -865,12 +688,10 @@ func Test_Parse(t *testing.T) { name: "wildcard ids and type and actions with list", input: "ids=*;type=*;actions=read,list", expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - ids: []string{"*"}, - typ: resource.All, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + ids: []string{"*"}, + typ: resource.All, actions: map[action.Type]bool{ action.Read: true, action.List: true, @@ -881,11 +702,9 @@ func Test_Parse(t *testing.T) { name: "good json type", input: `{"type":"host-catalog","actions":["create"]}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - typ: resource.HostCatalog, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + typ: resource.HostCatalog, actions: map[action.Type]bool{ action.Create: true, }, @@ -895,12 +714,10 @@ func Test_Parse(t *testing.T) { name: "good json id", input: `{"id":"u_foobar","actions":["read"]}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "u_foobar", - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "u_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -910,12 +727,10 @@ func Test_Parse(t *testing.T) { name: "good json ids", input: `{"ids":["hcst_foobar", "hcst_foobaz"],"actions":["read"]}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - ids: []string{"hcst_foobar", "hcst_foobaz"}, - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + ids: []string{"hcst_foobar", "hcst_foobaz"}, + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -925,12 +740,10 @@ func Test_Parse(t *testing.T) { name: "good json output fields id", input: `{"id":"u_foobar","actions":["read"],"output_fields":["version","id","name"]}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "u_foobar", - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "u_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -947,12 +760,10 @@ func Test_Parse(t *testing.T) { name: "good json output fields ids", input: `{"ids":["u_foobar"],"actions":["read"],"output_fields":["version","ids","name"]}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - ids: []string{"u_foobar"}, - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + ids: []string{"u_foobar"}, + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -969,12 +780,10 @@ func Test_Parse(t *testing.T) { name: "good json output fields no action", input: `{"id":"u_foobar","output_fields":["version","id","name"]}`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "u_foobar", - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "u_foobar", + typ: resource.Unknown, OutputFields: &OutputFields{ fields: map[string]bool{ "version": true, @@ -988,11 +797,9 @@ func Test_Parse(t *testing.T) { name: "good text type", input: `type=host-catalog;actions=create`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - typ: resource.HostCatalog, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + typ: resource.HostCatalog, actions: map[action.Type]bool{ action.Create: true, }, @@ -1002,12 +809,10 @@ func Test_Parse(t *testing.T) { name: "good text id", input: `id=u_foobar;actions=read`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "u_foobar", - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "u_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1017,12 +822,10 @@ func Test_Parse(t *testing.T) { name: "good text ids", input: `ids=hcst_foobar,hcst_foobaz;actions=read`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - ids: []string{"hcst_foobar", "hcst_foobaz"}, - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + ids: []string{"hcst_foobar", "hcst_foobaz"}, + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1032,12 +835,10 @@ func Test_Parse(t *testing.T) { name: "good output fields id", input: `id=u_foobar;actions=read;output_fields=version,id,name`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "u_foobar", - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "u_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1054,12 +855,10 @@ func Test_Parse(t *testing.T) { name: "good output fields ids", input: `ids=hcst_foobar,hcst_foobaz;actions=read;output_fields=version,ids,name`, expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - ids: []string{"hcst_foobar", "hcst_foobaz"}, - typ: resource.Unknown, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + ids: []string{"hcst_foobar", "hcst_foobaz"}, + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1077,12 +876,10 @@ func Test_Parse(t *testing.T) { input: `id=hcst_foobar;actions=read`, scopeOverride: "p_1234", expected: Grant{ - scope: Scope{ - Id: "p_1234", - Type: scope.Project, - }, - id: "hcst_foobar", - typ: resource.Unknown, + roleScopeId: "p_1234", + grantScopeId: "p_1234", + id: "hcst_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1093,12 +890,10 @@ func Test_Parse(t *testing.T) { input: `id=acctpw_foobar;actions=read`, scopeOverride: "o_1234", expected: Grant{ - scope: Scope{ - Id: "o_1234", - Type: scope.Org, - }, - id: "acctpw_foobar", - typ: resource.Unknown, + roleScopeId: "o_1234", + grantScopeId: "o_1234", + id: "acctpw_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1109,12 +904,10 @@ func Test_Parse(t *testing.T) { input: `id=acctpw_foobar;actions=read`, scopeOverride: "global", expected: Grant{ - scope: Scope{ - Id: "global", - Type: scope.Global, - }, - id: "acctpw_foobar", - typ: resource.Unknown, + roleScopeId: "global", + grantScopeId: "global", + id: "acctpw_foobar", + typ: resource.Unknown, actions: map[action.Type]bool{ action.Read: true, }, @@ -1137,11 +930,9 @@ func Test_Parse(t *testing.T) { input: `id={{ user.id}};actions=read,update`, userId: "u_abcd1234", expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: "u_abcd1234", + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: "u_abcd1234", actions: map[action.Type]bool{ action.Update: true, action.Read: true, @@ -1171,11 +962,9 @@ func Test_Parse(t *testing.T) { input: `id={{ account.id}};actions=update,read`, accountId: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix), expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix), + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix), actions: map[action.Type]bool{ action.Update: true, action.Read: true, @@ -1187,11 +976,9 @@ func Test_Parse(t *testing.T) { input: `id={{ account.id}};actions=update,read`, accountId: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix), expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix), + roleScopeId: "o_scope", + grantScopeId: "o_scope", + id: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix), actions: map[action.Type]bool{ action.Update: true, action.Read: true, @@ -1204,11 +991,9 @@ func Test_Parse(t *testing.T) { userId: "u_abcd1234", accountId: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix), expected: Grant{ - scope: Scope{ - Id: "o_scope", - Type: scope.Org, - }, - ids: []string{"u_abcd1234", "acctpw_1234567890"}, + roleScopeId: "o_scope", + grantScopeId: "o_scope", + ids: []string{"u_abcd1234", "acctpw_1234567890"}, actions: map[action.Type]bool{ action.Update: true, action.Read: true, @@ -1217,13 +1002,17 @@ func Test_Parse(t *testing.T) { }, } - _, err := Parse(ctx, "", "") + _, err := Parse(ctx, GrantTuple{RoleScopeId: "", GrantScopeId: "", Grant: ""}) require.Error(t, err) assert.Equal(t, "perms.Parse: missing grant string: parameter violation: error #100", err.Error()) - _, err = Parse(ctx, "", "{}") + _, err = Parse(ctx, GrantTuple{RoleScopeId: "", GrantScopeId: "", Grant: "{}"}) + require.Error(t, err) + assert.Equal(t, "perms.Parse: missing role scope id: parameter violation: error #100", err.Error()) + + _, err = Parse(ctx, GrantTuple{RoleScopeId: "p_abcd", GrantScopeId: "", Grant: "{}"}) require.Error(t, err) - assert.Equal(t, "perms.Parse: missing scope id: parameter violation: error #100", err.Error()) + assert.Equal(t, "perms.Parse: missing grant scope id: parameter violation: error #100", err.Error()) for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1233,7 +1022,7 @@ func Test_Parse(t *testing.T) { if test.scopeOverride != "" { scope = test.scopeOverride } - grant, err := Parse(ctx, scope, test.input, WithUserId(test.userId), WithAccountId(test.accountId)) + grant, err := Parse(ctx, GrantTuple{RoleScopeId: scope, GrantScopeId: scope, Grant: test.input}, WithUserId(test.userId), WithAccountId(test.accountId)) if test.err != "" { require.Error(err) assert.Equal(test.err, err.Error()) @@ -1329,11 +1118,11 @@ func FuzzParse(f *testing.F) { } f.Fuzz(func(t *testing.T, grant string) { - g, err := Parse(ctx, "global", grant, WithSkipFinalValidation(true)) + g, err := Parse(ctx, GrantTuple{GrantScopeId: "global", Grant: grant}, WithSkipFinalValidation(true)) if err != nil { return } - g2, err := Parse(ctx, "global", g.CanonicalString(), WithSkipFinalValidation(true)) + g2, err := Parse(ctx, GrantTuple{GrantScopeId: "global", Grant: g.CanonicalString()}, WithSkipFinalValidation(true)) if err != nil { t.Fatal("Failed to parse canonical string:", err) } @@ -1344,7 +1133,7 @@ func FuzzParse(f *testing.F) { if err != nil { t.Error("Failed to marshal JSON:", err) } - g3, err := Parse(ctx, "global", string(jsonBytes), WithSkipFinalValidation(true)) + g3, err := Parse(ctx, GrantTuple{GrantScopeId: "global", Grant: string(jsonBytes)}, WithSkipFinalValidation(true)) if err != nil { t.Fatal("Failed to parse json string:", err) } diff --git a/internal/perms/output_fields_test.go b/internal/perms/output_fields_test.go index 3aba89dcb5..e9d9c8825c 100644 --- a/internal/perms/output_fields_test.go +++ b/internal/perms/output_fields_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -149,14 +150,14 @@ func Test_ACLOutputFields(t *testing.T) { tests := []input{ { name: "default", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, action: action.Read, grants: []string{"ids=u_bar;actions=read,update"}, authorized: true, }, { name: "single value", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{"ids=u_bar;actions=read,update;output_fields=id"}, action: action.Read, fields: []string{"id"}, @@ -164,7 +165,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "compound no overlap", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read,update;output_fields=id", "ids=*;type=host-catalog;actions=read,update;output_fields=version", @@ -175,7 +176,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "compound", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read,update;output_fields=id", "ids=*;type=role;output_fields=version", @@ -186,7 +187,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "wildcard with type", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read,update;output_fields=read", "ids=*;type=role;output_fields=*", @@ -197,7 +198,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "wildcard with wildcard type", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read,update;output_fields=read", "ids=*;type=*;output_fields=*", @@ -208,7 +209,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "subaction exact", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read:self,update;output_fields=version", }, @@ -220,7 +221,7 @@ func Test_ACLOutputFields(t *testing.T) { // If the action is a subaction, parent output fields will apply, in // addition to subaction. This matches authorization. name: "subaction parent action", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read,update;output_fields=version", "ids=u_bar;actions=read:self;output_fields=id", @@ -235,7 +236,7 @@ func Test_ACLOutputFields(t *testing.T) { // non-self actions. This is useful to allow more visibility to self // actions and less in the general case. name: "subaction child action", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;actions=read:self,update;output_fields=version", "ids=u_bar;actions=read;output_fields=id", @@ -246,7 +247,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "initial grant unauthorized with star", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;output_fields=*", "ids=u_bar;actions=delete;output_fields=id", @@ -257,7 +258,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "unauthorized id only", - resource: Resource{ScopeId: "o_myorg", Id: "u_bar", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Id: "u_bar", Type: resource.Role}, grants: []string{ "ids=u_bar;output_fields=name", }, @@ -266,7 +267,7 @@ func Test_ACLOutputFields(t *testing.T) { }, { name: "unauthorized type only", - resource: Resource{ScopeId: "o_myorg", Type: resource.Role}, + resource: Resource{ScopeId: "o_myorg", ParentScopeId: scope.Global.String(), Type: resource.Role}, grants: []string{ "type=role;output_fields=name", }, @@ -279,7 +280,7 @@ func Test_ACLOutputFields(t *testing.T) { t.Run(test.name, func(t *testing.T) { var grants []Grant for _, g := range test.grants { - grant, err := Parse(ctx, "o_myorg", g) + grant, err := Parse(ctx, GrantTuple{RoleScopeId: "o_myorg", GrantScopeId: "o_myorg", Grant: g}) require.NoError(t, err) grants = append(grants, grant) } diff --git a/internal/session/repository.go b/internal/session/repository.go index 8f73923753..b7e98b56ef 100644 --- a/internal/session/repository.go +++ b/internal/session/repository.go @@ -98,7 +98,7 @@ func (r *Repository) listPermissionWhereClauses() ([]string, []any) { var clauses []string clauses = append(clauses, fmt.Sprintf("project_id = @project_id_%d", inClauseCnt)) - args = append(args, sql.Named(fmt.Sprintf("project_id_%d", inClauseCnt), p.ScopeId)) + args = append(args, sql.Named(fmt.Sprintf("project_id_%d", inClauseCnt), p.GrantScopeId)) if len(p.ResourceIds) > 0 { clauses = append(clauses, fmt.Sprintf("public_id = any(@public_id_%d)", inClauseCnt)) diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 001dbd0a66..9e28a90818 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -54,9 +54,9 @@ func TestRepository_ListSession(t *testing.T) { UserId: composedOf.UserId, Permissions: []perms.Permission{ { - ScopeId: composedOf.ProjectId, - Resource: resource.Session, - Action: action.List, + GrantScopeId: composedOf.ProjectId, + Resource: resource.Session, + Action: action.List, }, }, } @@ -109,9 +109,9 @@ func TestRepository_ListSession(t *testing.T) { perms: &perms.UserPermissions{ Permissions: []perms.Permission{ { - ScopeId: "o_thisIsNotValid", - Resource: resource.Session, - Action: action.List, + GrantScopeId: "o_thisIsNotValid", + Resource: resource.Session, + Action: action.List, }, }, }, @@ -126,9 +126,9 @@ func TestRepository_ListSession(t *testing.T) { perms: &perms.UserPermissions{ Permissions: []perms.Permission{ { - ScopeId: composedOf.ProjectId, - Resource: resource.Session, - Action: action.Read, + GrantScopeId: composedOf.ProjectId, + Resource: resource.Session, + Action: action.Read, }, }, }, @@ -200,10 +200,10 @@ func TestRepository_ListSession(t *testing.T) { UserId: s.UserId, Permissions: []perms.Permission{ { - ScopeId: s.ProjectId, - Resource: resource.Session, - Action: action.List, - OnlySelf: true, + GrantScopeId: s.ProjectId, + Resource: resource.Session, + Action: action.List, + OnlySelf: true, }, }, } @@ -227,9 +227,9 @@ func TestRepository_ListSession(t *testing.T) { UserId: composedOf.UserId, Permissions: []perms.Permission{ { - ScopeId: composedOf.ProjectId, - Resource: resource.Session, - Action: action.List, + GrantScopeId: composedOf.ProjectId, + Resource: resource.Session, + Action: action.List, }, }, } @@ -336,9 +336,9 @@ func TestRepository_ListSessions_Multiple_Scopes(t *testing.T) { for i := 0; i < numPerScope; i++ { composedOf := TestSessionParams(t, conn, wrapper, iamRepo) p = append(p, perms.Permission{ - ScopeId: composedOf.ProjectId, - Resource: resource.Session, - Action: action.List, + GrantScopeId: composedOf.ProjectId, + Resource: resource.Session, + Action: action.List, }) s := TestSession(t, conn, wrapper, composedOf) _ = TestState(t, conn, s.PublicId, StatusActive) diff --git a/internal/session/service_list_ext_test.go b/internal/session/service_list_ext_test.go index 539e1fc11e..175720a7bf 100644 --- a/internal/session/service_list_ext_test.go +++ b/internal/session/service_list_ext_test.go @@ -47,9 +47,9 @@ func TestService_List(t *testing.T) { UserId: composedOf.UserId, Permissions: []perms.Permission{ { - ScopeId: composedOf.ProjectId, - Resource: resource.Session, - Action: action.List, + GrantScopeId: composedOf.ProjectId, + Resource: resource.Session, + Action: action.List, }, }, } diff --git a/internal/target/options_test.go b/internal/target/options_test.go index 1069ba80e9..3e27f270a5 100644 --- a/internal/target/options_test.go +++ b/internal/target/options_test.go @@ -168,9 +168,9 @@ func Test_GetOpts(t *testing.T) { }) t.Run("WithPermissions", func(t *testing.T) { assert := assert.New(t) - opts := GetOpts(WithPermissions([]perms.Permission{{ScopeId: "test1"}, {ScopeId: "test2"}})) + opts := GetOpts(WithPermissions([]perms.Permission{{GrantScopeId: "test1"}, {GrantScopeId: "test2"}})) testOpts := getDefaultOptions() - testOpts.WithPermissions = []perms.Permission{{ScopeId: "test1"}, {ScopeId: "test2"}} + testOpts.WithPermissions = []perms.Permission{{GrantScopeId: "test1"}, {GrantScopeId: "test2"}} assert.Equal(opts, testOpts) }) t.Run("WithCredentialLibraries", func(t *testing.T) { diff --git a/internal/target/repository.go b/internal/target/repository.go index 1b27afb32c..c91624dba5 100644 --- a/internal/target/repository.go +++ b/internal/target/repository.go @@ -387,7 +387,7 @@ func (r *Repository) listPermissionWhereClauses() ([]string, []any) { var clauses []string clauses = append(clauses, fmt.Sprintf("project_id = @project_id_%d", inClauseCnt)) - args = append(args, sql.Named(fmt.Sprintf("project_id_%d", inClauseCnt), p.ScopeId)) + args = append(args, sql.Named(fmt.Sprintf("project_id_%d", inClauseCnt), p.GrantScopeId)) if len(p.ResourceIds) > 0 { clauses = append(clauses, fmt.Sprintf("public_id = any(@public_id_%d)", inClauseCnt)) diff --git a/internal/target/repository_ext_test.go b/internal/target/repository_ext_test.go index 53a5004c1f..9f253c62b3 100644 --- a/internal/target/repository_ext_test.go +++ b/internal/target/repository_ext_test.go @@ -155,16 +155,16 @@ func TestRepository_ListTargets(t *testing.T) { repo, err := target.NewRepository(ctx, rw, rw, testKms, target.WithPermissions([]perms.Permission{ { - ScopeId: proj1.PublicId, - Resource: resource.Target, - Action: action.List, - All: true, + GrantScopeId: proj1.PublicId, + Resource: resource.Target, + Action: action.List, + All: true, }, { - ScopeId: proj2.PublicId, - Resource: resource.Target, - Action: action.List, - All: true, + GrantScopeId: proj2.PublicId, + Resource: resource.Target, + Action: action.List, + All: true, }, }), ) @@ -327,16 +327,16 @@ func TestRepository_ListTargets_Multiple_Scopes(t *testing.T) { repo, err := target.NewRepository(ctx, rw, rw, testKms, target.WithPermissions([]perms.Permission{ { - ScopeId: proj1.PublicId, - Resource: resource.Target, - Action: action.List, - All: true, + GrantScopeId: proj1.PublicId, + Resource: resource.Target, + Action: action.List, + All: true, }, { - ScopeId: proj2.PublicId, - Resource: resource.Target, - Action: action.List, - All: true, + GrantScopeId: proj2.PublicId, + Resource: resource.Target, + Action: action.List, + All: true, }, }), ) @@ -371,10 +371,10 @@ func TestRepository_ListRoles_Above_Default_Count(t *testing.T) { repo, err := target.NewRepository(ctx, rw, rw, testKms, target.WithPermissions([]perms.Permission{ { - ScopeId: proj.PublicId, - Resource: resource.Target, - Action: action.List, - All: true, + GrantScopeId: proj.PublicId, + Resource: resource.Target, + Action: action.List, + All: true, }, })) require.NoError(t, err) diff --git a/internal/target/repository_test.go b/internal/target/repository_test.go index 4663fae122..237537cd05 100644 --- a/internal/target/repository_test.go +++ b/internal/target/repository_test.go @@ -91,8 +91,8 @@ func TestNewRepository(t *testing.T) { kms: testKms, opts: []Option{ WithPermissions([]perms.Permission{ - {ScopeId: "test1", Resource: resource.Target}, - {ScopeId: "test2", Resource: resource.Target}, + {GrantScopeId: "test1", Resource: resource.Target}, + {GrantScopeId: "test2", Resource: resource.Target}, }), }, }, @@ -102,8 +102,8 @@ func TestNewRepository(t *testing.T) { kms: testKms, defaultLimit: db.DefaultLimit, permissions: []perms.Permission{ - {ScopeId: "test1", Resource: resource.Target}, - {ScopeId: "test2", Resource: resource.Target}, + {GrantScopeId: "test1", Resource: resource.Target}, + {GrantScopeId: "test2", Resource: resource.Target}, }, }, wantErr: false, @@ -116,8 +116,8 @@ func TestNewRepository(t *testing.T) { kms: testKms, opts: []Option{ WithPermissions([]perms.Permission{ - {ScopeId: "test1", Resource: resource.Target}, - {ScopeId: "test2", Resource: resource.Host}, + {GrantScopeId: "test1", Resource: resource.Target}, + {GrantScopeId: "test2", Resource: resource.Host}, }), }, }, diff --git a/internal/target/service_list_ext_test.go b/internal/target/service_list_ext_test.go index ee23b2e752..7be5f01c55 100644 --- a/internal/target/service_list_ext_test.go +++ b/internal/target/service_list_ext_test.go @@ -65,10 +65,10 @@ func TestService_List(t *testing.T) { repo, err := target.NewRepository(ctx, rw, rw, testKms, target.WithPermissions([]perms.Permission{ { - ScopeId: proj1.PublicId, - Resource: resource.Target, - Action: action.List, - All: true, + GrantScopeId: proj1.PublicId, + Resource: resource.Target, + Action: action.List, + All: true, }, }), ) diff --git a/internal/tests/api/users/user_test.go b/internal/tests/api/users/user_test.go index b2243cbc1d..06353ba996 100644 --- a/internal/tests/api/users/user_test.go +++ b/internal/tests/api/users/user_test.go @@ -187,7 +187,7 @@ func TestListResolvableAliases(t *testing.T) { tarClient := targets.NewClient(client) resp, err := tarClient.List(tc.Context(), "global", targets.WithRecursive(true)) require.NoError(err) - assert.Len(resp.Items, 2) + require.Len(resp.Items, 2) firstTargetId := resp.Items[0].Id secondTargetId := resp.Items[1].Id