diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d4882234..63b0ce8cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,4 +58,34 @@ jobs: env: POSTGRES_PASSWORD: postgres run: make e2e-smoke-test + e2e-regression-test: + runs-on: ubuntu-latest + services: + echo-server: + image: ealen/echo-server + ports: + - "4000:80" + spicedb: + image: quay.io/authzed/spicedb:v1.0.0 + ports: + - "8080:8080" + - "50051:50051" + - "50053:50053" + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.21' + - name: install dependencies + run: go mod tidy + - name: install spicedb binary + uses: authzed/action-spicedb@v1 + - name: run regression tests + env: + POSTGRES_PASSWORD: postgres + run: make e2e-regression-test \ No newline at end of file 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/attribute/attribute.go b/internal/proxy/attribute/attribute.go new file mode 100644 index 000000000..69f023035 --- /dev/null +++ b/internal/proxy/attribute/attribute.go @@ -0,0 +1,40 @@ +package attribute + +import ( + "strings" + + "github.com/valyala/fasttemplate" +) + +const ( + TypeJSONPayload AttributeType = "json_payload" + TypeGRPCPayload AttributeType = "grpc_payload" + TypeQuery AttributeType = "query" + TypeHeader AttributeType = "header" + TypePathParam AttributeType = "path_param" + TypeConstant AttributeType = "constant" + TypeComposite AttributeType = "composite" + + SourceRequest AttributeType = "request" + SourceResponse AttributeType = "response" +) + +type AttributeType string + +type Attribute struct { + Key string `yaml:"key" mapstructure:"key"` + Type AttributeType `yaml:"type" mapstructure:"type"` + Index string `yaml:"index" mapstructure:"index"` // proto index + Path string `yaml:"path" mapstructure:"path"` + Params []string `yaml:"params" mapstructure:"params"` + Source string `yaml:"source" mapstructure:"source"` + Value string `yaml:"value" mapstructure:"value"` +} + +func Compose(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..479bb3259 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" + proxyattr "github.com/goto/shield/internal/proxy/attribute" "github.com/goto/shield/internal/proxy/hook" "github.com/goto/shield/internal/proxy/middleware" "github.com/goto/shield/pkg/body_extractor" @@ -92,9 +93,9 @@ type Relation struct { } type Config struct { - Action string `yaml:"action" mapstructure:"action"` - Attributes map[string]hook.Attribute `yaml:"attributes" mapstructure:"attributes"` - Relations []Relation `yaml:"relations" mapstructure:"relations"` + Action string `yaml:"action" mapstructure:"action"` + Attributes map[string]proxyattr.Attribute `yaml:"attributes" mapstructure:"attributes"` + Relations []Relation `yaml:"relations" mapstructure:"relations"` } func (a Authz) Info() hook.Info { @@ -151,17 +152,17 @@ func (a Authz) ServeHook(res *http.Response, err error) (*http.Response, error) for id, attr := range config.Attributes { bdy, _ := middleware.ExtractRequestBody(res.Request) bodySource := &res.Body - if attr.Source == string(hook.SourceRequest) { + if attr.Source == string(proxyattr.SourceRequest) { bodySource = &bdy } headerSource := &res.Header - if attr.Source == string(hook.SourceRequest) { + if attr.Source == string(proxyattr.SourceRequest) { headerSource = &res.Request.Header } switch attr.Type { - case hook.AttributeTypeGRPCPayload: + case proxyattr.TypeGRPCPayload: if !strings.HasPrefix(res.Header.Get("Content-Type"), "application/grpc") { a.log.Error("middleware: not a grpc request", "attr", attr) return a.escape.ServeHook(res, fmt.Errorf("invalid header for http request: %s", res.Header.Get("Content-Type"))) @@ -175,7 +176,7 @@ func (a Authz) ServeHook(res *http.Response, err error) (*http.Response, error) attributes[id] = payloadField a.log.Info("middleware: extracted", "field", payloadField, "attr", attr) - case hook.AttributeTypeJSONPayload: + case proxyattr.TypeJSONPayload: if attr.Key == "" { a.log.Error("middleware: payload key field empty") return a.escape.ServeHook(res, fmt.Errorf("payload key field empty")) @@ -189,7 +190,7 @@ func (a Authz) ServeHook(res *http.Response, err error) (*http.Response, error) attributes[id] = payloadField a.log.Info("middleware: extracted", "field", payloadField, "attr", attr) - case hook.AttributeTypeHeader: + case proxyattr.TypeHeader: if attr.Key == "" { a.log.Error("middleware: header key field empty") return a.escape.ServeHook(res, fmt.Errorf("failed to parse json payload")) @@ -203,7 +204,7 @@ func (a Authz) ServeHook(res *http.Response, err error) (*http.Response, error) attributes[id] = headerAttr a.log.Info("middleware: extracted", "field", headerAttr, "attr", attr) - case hook.AttributeTypeQuery: + case proxyattr.TypeQuery: if attr.Key == "" { a.log.Error("middleware: query key field empty") return a.escape.ServeHook(res, fmt.Errorf("failed to parse json payload")) @@ -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 proxyattr.TypeConstant, proxyattr.TypeComposite: 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, proxyattr.Compose(res, permissionAttributes)) + } + return resourcesName +} diff --git a/internal/proxy/hook/authz/authz_test.go b/internal/proxy/hook/authz/authz_test.go index 1691b66e7..de3b12f6d 100644 --- a/internal/proxy/hook/authz/authz_test.go +++ b/internal/proxy/hook/authz/authz_test.go @@ -16,6 +16,7 @@ import ( "github.com/goto/shield/core/relation" "github.com/goto/shield/core/resource" "github.com/goto/shield/core/rule" + "github.com/goto/shield/internal/proxy/attribute" "github.com/goto/shield/internal/proxy/hook" "github.com/goto/shield/internal/proxy/hook/authz/mocks" shieldlogger "github.com/goto/shield/pkg/logger" @@ -275,7 +276,7 @@ func TestServeHook(t *testing.T) { rule.HookSpec{ Name: "authz", Config: map[string]interface{}{ - "attributes": map[string]hook.Attribute{ + "attributes": map[string]attribute.Attribute{ "project": { Type: "constant", Value: testPermissionAttributesMap["project"].(string), @@ -346,7 +347,7 @@ func TestServeHook(t *testing.T) { rule.HookSpec{ Name: "authz", Config: map[string]interface{}{ - "attributes": map[string]hook.Attribute{ + "attributes": map[string]attribute.Attribute{ "project": { Type: "constant", Value: testPermissionAttributesMap["project"].(string), @@ -442,7 +443,7 @@ func TestServeHook(t *testing.T) { rule.HookSpec{ Name: "authz", Config: map[string]interface{}{ - "attributes": map[string]hook.Attribute{ + "attributes": map[string]attribute.Attribute{ "project": { Type: "constant", Value: testPermissionAttributesMap["project"].(string), @@ -541,7 +542,7 @@ func TestServeHook(t *testing.T) { rule.HookSpec{ Name: "authz", Config: map[string]interface{}{ - "attributes": map[string]hook.Attribute{ + "attributes": map[string]attribute.Attribute{ "project": { Type: "constant", Value: testPermissionAttributesMap["project"].(string), @@ -641,7 +642,7 @@ func TestServeHook(t *testing.T) { rule.HookSpec{ Name: "authz", Config: map[string]interface{}{ - "attributes": map[string]hook.Attribute{ + "attributes": map[string]attribute.Attribute{ "project": { Type: "constant", Value: testPermissionAttributesMap["project"].(string), diff --git a/internal/proxy/hook/hook.go b/internal/proxy/hook/hook.go index 2eb71f198..bc30cfdda 100644 --- a/internal/proxy/hook/hook.go +++ b/internal/proxy/hook/hook.go @@ -17,27 +17,6 @@ type Info struct { Description string } -const ( - AttributeTypeJSONPayload AttributeType = "json_payload" - AttributeTypeGRPCPayload AttributeType = "grpc_payload" - AttributeTypeQuery AttributeType = "query" - AttributeTypeHeader AttributeType = "header" - AttributeTypeConstant AttributeType = "constant" - - SourceRequest AttributeType = "request" - SourceResponse AttributeType = "response" -) - -type AttributeType string - -type Attribute struct { - Key string `yaml:"key" mapstructure:"key"` - Type AttributeType `yaml:"type" mapstructure:"type"` - Index string `yaml:"index" mapstructure:"index"` // proto index - Source string `yaml:"source" mapstructure:"source"` - Value string `yaml:"value" mapstructure:"value"` -} - func ExtractHook(r *http.Request, name string) (rule.HookSpec, bool) { rl, ok := ExtractRule(r) if !ok { diff --git a/internal/proxy/middleware/attribute.go b/internal/proxy/middleware/attribute.go deleted file mode 100644 index 080efedfe..000000000 --- a/internal/proxy/middleware/attribute.go +++ /dev/null @@ -1,21 +0,0 @@ -package middleware - -const ( - AttributeTypeQuery AttributeType = "query" - AttributeTypeHeader AttributeType = "header" - AttributeTypeJSONPayload AttributeType = "json_payload" - AttributeTypeGRPCPayload AttributeType = "grpc_payload" - AttributeTypePathParam AttributeType = "path_param" - AttributeTypeConstant AttributeType = "constant" -) - -type AttributeType string - -type Attribute struct { - Key string `yaml:"key" mapstructure:"key"` - Type AttributeType `yaml:"type" mapstructure:"type"` - Index string `yaml:"index" mapstructure:"index"` // proto index - Path string `yaml:"path" mapstructure:"path"` - Params []string `yaml:"params" mapstructure:"params"` - Value string `yaml:"value" mapstructure:"value"` -} diff --git a/internal/proxy/middleware/attributes/attributes.go b/internal/proxy/middleware/attributes/attributes.go index 67f2bc21a..1bc92e95f 100644 --- a/internal/proxy/middleware/attributes/attributes.go +++ b/internal/proxy/middleware/attributes/attributes.go @@ -12,6 +12,7 @@ import ( "github.com/goto/salt/log" "github.com/goto/shield/core/project" + "github.com/goto/shield/internal/proxy/attribute" "github.com/goto/shield/internal/proxy/middleware" "github.com/goto/shield/pkg/body_extractor" ) @@ -24,7 +25,7 @@ type Attributes struct { } type Config struct { - Attributes map[string]middleware.Attribute `yaml:"attributes" mapstructure:"attributes"` + Attributes map[string]attribute.Attribute `yaml:"attributes" mapstructure:"attributes"` } type ProjectService interface { @@ -86,7 +87,7 @@ func (a *Attributes) ServeHTTP(rw http.ResponseWriter, req *http.Request) { _ = res switch attr.Type { - case middleware.AttributeTypeGRPCPayload: + case attribute.TypeGRPCPayload: // check if grpc request if !strings.HasPrefix(req.Header.Get("Content-Type"), "application/grpc") { a.log.Error("middleware: not a grpc request", "attr", attr) @@ -105,7 +106,7 @@ func (a *Attributes) ServeHTTP(rw http.ResponseWriter, req *http.Request) { requestAttributes[res] = payloadField a.log.Info("middleware: extracted", "field", payloadField, "attr", attr) - case middleware.AttributeTypeJSONPayload: + case attribute.TypeJSONPayload: if attr.Key == "" { a.log.Error("middleware: payload key field empty") a.notAllowed(rw) @@ -121,7 +122,7 @@ func (a *Attributes) ServeHTTP(rw http.ResponseWriter, req *http.Request) { requestAttributes[res] = payloadField a.log.Info("middleware: extracted", "field", payloadField, "attr", attr) - case middleware.AttributeTypeHeader: + case attribute.TypeHeader: if attr.Key == "" { a.log.Error("middleware: header key field empty") a.notAllowed(rw) @@ -137,7 +138,7 @@ func (a *Attributes) ServeHTTP(rw http.ResponseWriter, req *http.Request) { requestAttributes[res] = headerAttr a.log.Info("middleware: extracted", "field", headerAttr, "attr", attr) - case middleware.AttributeTypeQuery: + case attribute.TypeQuery: if attr.Key == "" { a.log.Error("middleware: query key field empty") a.notAllowed(rw) @@ -153,7 +154,7 @@ func (a *Attributes) ServeHTTP(rw http.ResponseWriter, req *http.Request) { requestAttributes[res] = queryAttr a.log.Info("middleware: extracted", "field", queryAttr, "attr", attr) - case middleware.AttributeTypeConstant: + case attribute.TypeConstant: if attr.Value == "" { a.log.Error("middleware: constant value empty") a.notAllowed(rw) diff --git a/internal/proxy/middleware/authz/authz.go b/internal/proxy/middleware/authz/authz.go index 7d75d3fd8..4f559f78c 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/attribute" "github.com/goto/shield/internal/proxy/middleware" "github.com/goto/shield/internal/schema" "github.com/goto/shield/pkg/body_extractor" @@ -43,9 +44,9 @@ type Authz struct { } type Config struct { - Actions []string `yaml:"actions" mapstructure:"actions"` - Permissions []Permission `yaml:"permissions" mapstructure:"permissions"` - Attributes map[string]middleware.Attribute `yaml:"attributes" mapstructure:"attributes"` + Actions []string `yaml:"actions" mapstructure:"actions"` + Permissions []Permission `yaml:"permissions" mapstructure:"permissions"` + Attributes map[string]attribute.Attribute `yaml:"attributes" mapstructure:"attributes"` } type Permission struct { @@ -132,7 +133,7 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { _ = res switch attr.Type { - case middleware.AttributeTypeGRPCPayload: + case attribute.TypeGRPCPayload: // check if grpc request if !strings.HasPrefix(req.Header.Get("Content-Type"), "application/grpc") { c.log.Error("middleware: not a grpc request", "attr", attr) @@ -150,7 +151,7 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { permissionAttributes[res] = payloadField c.log.Info("middleware: extracted", "field", payloadField, "attr", attr) - case middleware.AttributeTypeJSONPayload: + case attribute.TypeJSONPayload: if attr.Key == "" { c.log.Error("middleware: payload key field empty") c.notAllowed(rw, nil) @@ -166,7 +167,7 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { permissionAttributes[res] = payloadField c.log.Info("middleware: extracted", "field", payloadField, "attr", attr) - case middleware.AttributeTypeHeader: + case attribute.TypeHeader: if attr.Key == "" { c.log.Error("middleware: header key field empty") c.notAllowed(rw, nil) @@ -182,7 +183,7 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { permissionAttributes[res] = headerAttr c.log.Info("middleware: extracted", "field", headerAttr, "attr", attr) - case middleware.AttributeTypeQuery: + case attribute.TypeQuery: if attr.Key == "" { c.log.Error("middleware: query key field empty") c.notAllowed(rw, nil) @@ -198,7 +199,7 @@ func (c *Authz) ServeHTTP(rw http.ResponseWriter, req *http.Request) { permissionAttributes[res] = queryAttr c.log.Info("middleware: extracted", "field", queryAttr, "attr", attr) - case middleware.AttributeTypeConstant: + case attribute.TypeConstant: if attr.Value == "" { c.log.Error("middleware: constant value empty") c.notAllowed(rw, nil) @@ -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 = attribute.Compose(perm.Attribute, attrs) + } + res := resource.Resource{ Name: resourceName, NamespaceID: perm.Namespace, diff --git a/internal/proxy/middleware/basic_auth/auth.go b/internal/proxy/middleware/basic_auth/auth.go index 585abd8f8..1edfdb75c 100644 --- a/internal/proxy/middleware/basic_auth/auth.go +++ b/internal/proxy/middleware/basic_auth/auth.go @@ -7,6 +7,7 @@ import ( "strings" "text/template" + "github.com/goto/shield/internal/proxy/attribute" "github.com/goto/shield/internal/proxy/middleware" "github.com/goto/shield/pkg/body_extractor" "github.com/goto/shield/pkg/httputil" @@ -53,8 +54,8 @@ type Credentials struct { } type Scope struct { - Action string `yaml:"action" mapstructure:"action"` - Attributes map[string]middleware.Attribute `yaml:"attributes" mapstructure:"attributes"` // auth field -> Attribute + Action string `yaml:"action" mapstructure:"action"` + Attributes map[string]attribute.Attribute `yaml:"attributes" mapstructure:"attributes"` // auth field -> Attribute } func New(logger log.Logger, next http.Handler) *BasicAuth { @@ -139,7 +140,7 @@ func (w BasicAuth) authorizeRequest(conf Config, user string, req *http.Request) for res, attr := range conf.Scope.Attributes { templateMap[res] = "" switch attr.Type { - case middleware.AttributeTypeGRPCPayload: + case attribute.TypeGRPCPayload: // check if grpc request if !strings.HasPrefix(req.Header.Get("Content-Type"), "application/grpc") { w.log.Error("middleware: not a valid grpc request") @@ -155,7 +156,7 @@ func (w BasicAuth) authorizeRequest(conf Config, user string, req *http.Request) templateMap[res] = payloadField w.log.Debug("middleware: extracted", "field", payloadField, "attr", attr) - case middleware.AttributeTypeJSONPayload: + case attribute.TypeJSONPayload: if attr.Key == "" { w.log.Error("middleware: payload key field empty") return false 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..390a0df36 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:64610" 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: view + 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..7343720f6 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: view + namespace: entropy/firehose + attribute: ${project}-${name} \ No newline at end of file