Skip to content

Commit

Permalink
feat: enable resource identifier composing from multiple variables
Browse files Browse the repository at this point in the history
  • Loading branch information
FemiNoviaLina committed Jul 3, 2024
1 parent ea97a84 commit 6303132
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 6 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
15 changes: 15 additions & 0 deletions internal/proxy/helper.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 15 additions & 4 deletions internal/proxy/hook/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions internal/proxy/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
AttributeTypeQuery AttributeType = "query"
AttributeTypeHeader AttributeType = "header"
AttributeTypeConstant AttributeType = "constant"
AttributeTypeComposite AttributeType = "composite"

SourceRequest AttributeType = "request"
SourceResponse AttributeType = "response"
Expand Down
8 changes: 7 additions & 1 deletion internal/proxy/middleware/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
})
Expand All @@ -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,
Expand Down
74 changes: 74 additions & 0 deletions test/e2e_test/smoke/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "[email protected]")
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) {
Expand Down
2 changes: 2 additions & 0 deletions test/e2e_test/testbench/mockserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
47 changes: 46 additions & 1 deletion test/e2e_test/testbench/testdata/configs/rules/rule.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
rules:
- backends:
- name: entropy
target: "http://localhost:55702"
target: "http://localhost:51612"
frontends:
- name: ping
path: "/api/ping"
Expand Down Expand Up @@ -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}'
45 changes: 45 additions & 0 deletions test/e2e_test/testbench/testdata/configs/rules/rule.yamltpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}'

0 comments on commit 6303132

Please sign in to comment.