diff --git a/go.mod b/go.mod index bc838217c..72918008e 100644 --- a/go.mod +++ b/go.mod @@ -157,6 +157,8 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 github.com/yuin/goldmark v1.5.3 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect diff --git a/go.sum b/go.sum index 5e8604064..62a262417 100644 --- a/go.sum +++ b/go.sum @@ -1956,6 +1956,10 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= diff --git a/internal/proxy/helper.go b/internal/proxy/helper.go new file mode 100644 index 000000000..507a00189 --- /dev/null +++ b/internal/proxy/helper.go @@ -0,0 +1,15 @@ +package proxy + +import ( + "strings" + + "github.com/valyala/fasttemplate" +) + +func ComposeAttribute(attribute string, attrs map[string]interface{}) string { + if strings.Contains(attribute, "{") { + template := fasttemplate.New(attribute, "{", "}") + return template.ExecuteString(attrs) + } + return attribute +} diff --git a/internal/proxy/hook/authz/authz.go b/internal/proxy/hook/authz/authz.go index 019632f18..d1433c9c6 100644 --- a/internal/proxy/hook/authz/authz.go +++ b/internal/proxy/hook/authz/authz.go @@ -18,6 +18,7 @@ import ( "github.com/goto/shield/core/relation" "github.com/goto/shield/core/resource" "github.com/goto/shield/core/user" + "github.com/goto/shield/internal/proxy" "github.com/goto/shield/internal/proxy/hook" "github.com/goto/shield/internal/proxy/middleware" "github.com/goto/shield/pkg/body_extractor" @@ -217,14 +218,14 @@ func (a Authz) ServeHook(res *http.Response, err error) (*http.Response, error) attributes[id] = queryAttr a.log.Info("middleware: extracted", "field", queryAttr, "attr", attr) - case hook.AttributeTypeConstant: + case hook.AttributeTypeConstant, hook.AttributeTypeComposite: if attr.Value == "" { - a.log.Error("middleware: constant value empty") + a.log.Error("middleware:", string(attr.Type), "value empty") return a.escape.ServeHook(res, fmt.Errorf("failed to parse json payload")) } attributes[id] = attr.Value - a.log.Info("middleware: extracted", "constant_key", res, "attr", attributes[id]) + a.log.Info("middleware: extracted", "key", res, "attr", attributes[id]) default: a.log.Error("middleware: unknown attribute type", "attr", attr) @@ -347,9 +348,11 @@ func (a Authz) createResources(permissionAttributes map[string]interface{}) ([]r return nil, fmt.Errorf("namespace, resource type, projects, resource, and team are required") } + resourcesName := composeResourcesName(resourceList, permissionAttributes) + // TODO(krtkvrm): needs revision for _, project := range projects { - for _, res := range resourceList { + for _, res := range resourcesName { resources = append(resources, resource.Resource{ Name: res, ProjectID: project, @@ -389,3 +392,11 @@ func getAttributesValues(attributes interface{}) ([]string, error) { } return values, nil } + +func composeResourcesName(resourceList []string, permissionAttributes map[string]interface{}) []string { + var resourcesName []string + for _, res := range resourceList { + resourcesName = append(resourcesName, proxy.ComposeAttribute(res, permissionAttributes)) + } + return resourcesName +} diff --git a/internal/proxy/hook/hook.go b/internal/proxy/hook/hook.go index 2eb71f198..fa405c95f 100644 --- a/internal/proxy/hook/hook.go +++ b/internal/proxy/hook/hook.go @@ -23,6 +23,7 @@ const ( AttributeTypeQuery AttributeType = "query" AttributeTypeHeader AttributeType = "header" AttributeTypeConstant AttributeType = "constant" + AttributeTypeComposite AttributeType = "composite" SourceRequest AttributeType = "request" SourceResponse AttributeType = "response" diff --git a/internal/proxy/middleware/authz/authz.go b/internal/proxy/middleware/authz/authz.go index 7d75d3fd8..587f19c1c 100644 --- a/internal/proxy/middleware/authz/authz.go +++ b/internal/proxy/middleware/authz/authz.go @@ -14,6 +14,7 @@ import ( "github.com/goto/shield/core/group" "github.com/goto/shield/core/resource" "github.com/goto/shield/core/user" + "github.com/goto/shield/internal/proxy" "github.com/goto/shield/internal/proxy/middleware" "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/body_extractor" @@ -250,6 +251,7 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { c.notAllowed(rw, err) return } + isAuthorized, err = c.resourceService.CheckAuthz(req.Context(), res, action.Action{ ID: permission.Name, }) @@ -275,7 +277,11 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } func (c Authz) preparePermissionResource(ctx context.Context, perm Permission, attrs map[string]interface{}) (resource.Resource, error) { - resourceName := attrs[perm.Attribute].(string) + resourceName, ok := attrs[perm.Attribute].(string) + if !ok { + resourceName = proxy.ComposeAttribute(perm.Attribute, attrs) + } + res := resource.Resource{ Name: resourceName, NamespaceID: perm.Namespace, diff --git a/test/e2e_test/smoke/proxy_test.go b/test/e2e_test/smoke/proxy_test.go index 092798d5b..c8e14cc23 100644 --- a/test/e2e_test/smoke/proxy_test.go +++ b/test/e2e_test/smoke/proxy_test.go @@ -424,6 +424,80 @@ func (s *EndToEndProxySmokeTestSuite) TestProxyToEchoServer() { } s.Assert().Equal(s.userID, subjectID) }) + s.Run("resource created on echo server should persist in shieldDB when using composite variable", func() { + userDetail, err := s.client.GetUser(context.Background(), &shieldv1beta1.GetUserRequest{Id: s.userID}) + s.Require().NoError(err) + + url := fmt.Sprintf("http://localhost:%d/api/resource_composite/test-name", s.appConfig.Proxy.Services[0].Port) + reqBodyMap := map[string]string{ + "project": s.projID, + "user_email": userDetail.GetUser().GetEmail(), + } + reqBodyBytes, err := json.Marshal(reqBodyMap) + s.Require().NoError(err) + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBodyBytes)) + s.Require().NoError(err) + + req.Header.Set(testbench.IdentityHeader, "member2-group1@gotocompany.com") + req.Header.Set("X-Shield-Org", s.orgID) + + res, err := http.DefaultClient.Do(req) + s.Require().NoError(err) + + defer res.Body.Close() + + s.Assert().Equal(200, res.StatusCode) + + resourceSelectQuery := "SELECT name FROM resources" + resources, err := s.dbClient.Query(resourceSelectQuery) + s.Require().NoError(err) + defer resources.Close() + + resourceName := "" + for resources.Next() { + if err := resources.Scan(&resourceName); err != nil { + s.Require().NoError(err) + } + } + s.Assert().Equal(fmt.Sprintf("%s-test-name", s.projID), resourceName) + + relationSelectQuery := "SELECT subject_id FROM relations ORDER BY created_at DESC LIMIT 1" + relations, err := s.dbClient.Query(relationSelectQuery) + s.Require().NoError(err) + defer resources.Close() + + subjectID := "" + for relations.Next() { + if err := relations.Scan(&subjectID); err != nil { + s.Require().NoError(err) + } + } + s.Assert().Equal(s.userID, subjectID) + }) + s.Run("permission expression: permission resource can be composed using multiple variable", func() { + userDetail, err := s.client.GetUser(context.Background(), &shieldv1beta1.GetUserRequest{Id: s.userID}) + s.Require().NoError(err) + + url := fmt.Sprintf("http://localhost:%d/api/update_firehose_based_on_sink/test-name", s.appConfig.Proxy.Services[0].Port) + reqBodyMap := map[string]any{ + "organization": s.orgSlug, + "project": s.projID, + } + reqBodyBytes, err := json.Marshal(reqBodyMap) + s.Require().NoError(err) + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBodyBytes)) + s.Require().NoError(err) + + req.Header.Set(testbench.IdentityHeader, userDetail.GetUser().GetEmail()) + + res, err := http.DefaultClient.Do(req) + s.Require().NoError(err) + + defer res.Body.Close() + s.Assert().Equal(200, res.StatusCode) + }) } func TestEndToEndProxySmokeTestSuite(t *testing.T) { diff --git a/test/e2e_test/testbench/mockserver.go b/test/e2e_test/testbench/mockserver.go index f29ceca1b..8b6b7e6c5 100644 --- a/test/e2e_test/testbench/mockserver.go +++ b/test/e2e_test/testbench/mockserver.go @@ -57,6 +57,8 @@ func startMockServer(ctx context.Context, logger *log.Zap, port int) { router.POST("/api/resource_slug", createResourceFn) router.POST("/api/resource_user_id", createResourceFn) router.POST("/api/resource_user_email", createResourceFn) + router.POST("/api/resource_composite/:name", createResourceFn) + router.POST("/api/update_firehose_based_on_sink/:name", createResourceFn) logger.Info("starting up mock server...", "port", port) if err := http.ListenAndServe(fmt.Sprintf(":%d", port), router); err != nil && !errors.Is(err, http.ErrServerClosed) { diff --git a/test/e2e_test/testbench/testdata/configs/rules/rule.yaml b/test/e2e_test/testbench/testdata/configs/rules/rule.yaml index 5ec426517..e7db87f6c 100644 --- a/test/e2e_test/testbench/testdata/configs/rules/rule.yaml +++ b/test/e2e_test/testbench/testdata/configs/rules/rule.yaml @@ -1,7 +1,7 @@ rules: - backends: - name: entropy - target: "http://localhost:55702" + target: "http://localhost:51612" frontends: - name: ping path: "/api/ping" @@ -172,3 +172,48 @@ rules: - role: owner subject_principal: shield/user subject_id_attribute: user + - name: create_resource_composite + path: /api/resource_composite/{name} + method: "POST" + hooks: + - name: authz + config: + action: authz_action + attributes: + resource: + value: '{project}-{name}' + type: composite + project: + key: project + type: json_payload + source: request + user: + key: user_email + type: json_payload + source: request + organization: + key: X-Shield-Org + type: header + source: request + resource_type: + value: "firehose" + type: constant + relations: + - role: owner + subject_principal: shield/user + subject_id_attribute: user + - name: update_firehose_based_on_sink_composite + path: /api/update_firehose_based_on_sink/{name} + method: "POST" + middlewares: + - name: authz + config: + attributes: + project: + key: project + type: json_payload + source: request + permissions: + - name: owner + namespace: entropy/firehose + attribute: '{project}-{name}' \ No newline at end of file diff --git a/test/e2e_test/testbench/testdata/configs/rules/rule.yamltpl b/test/e2e_test/testbench/testdata/configs/rules/rule.yamltpl index abb622518..8d5431260 100644 --- a/test/e2e_test/testbench/testdata/configs/rules/rule.yamltpl +++ b/test/e2e_test/testbench/testdata/configs/rules/rule.yamltpl @@ -172,3 +172,48 @@ rules: - role: owner subject_principal: shield/user subject_id_attribute: user + - name: create_resource_composite + path: /api/resource_composite/{name} + method: "POST" + hooks: + - name: authz + config: + action: authz_action + attributes: + resource: + value: '{project}-{name}' + type: composite + project: + key: project + type: json_payload + source: request + user: + key: user_email + type: json_payload + source: request + organization: + key: X-Shield-Org + type: header + source: request + resource_type: + value: "firehose" + type: constant + relations: + - role: owner + subject_principal: shield/user + subject_id_attribute: user + - name: update_firehose_based_on_sink_composite + path: /api/update_firehose_based_on_sink/{name} + method: "POST" + middlewares: + - name: authz + config: + attributes: + project: + key: project + type: json_payload + source: request + permissions: + - name: owner + namespace: entropy/firehose + attribute: '{project}-{name}' \ No newline at end of file