Skip to content

Commit

Permalink
feat: simpler notation for subjects w/o relation
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl authored and zepatrik committed Aug 24, 2022
1 parent 065ce46 commit ec979df
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 26 deletions.
14 changes: 8 additions & 6 deletions internal/check/rewrites_test.go
Expand Up @@ -37,6 +37,7 @@ var namespaces = []*namespace.Namespace{
Relation: "parent",
ComputedSubjectSetRelation: "viewer"}}}},
}},
{Name: "users"},
{Name: "group",
Relations: []ast.Relation{{Name: "member"}},
},
Expand Down Expand Up @@ -103,9 +104,9 @@ func TestUsersetRewrites(t *testing.T) {
reg.Logger().Logger.SetLevel(logrus.TraceLevel)

insertFixtures(t, reg.RelationTupleManager(), []string{
"doc:document#owner@user", // user owns doc
"doc:doc_in_folder#parent@doc:folder#", // doc_in_folder is in folder
"doc:folder#owner@user", // user owns folder
"doc:document#owner@users:user#...", // user owns doc
"doc:doc_in_folder#parent@doc:folder#...", // doc_in_folder is in folder
"doc:folder#owner@users:user#...", // user owns folder

// Folder hierarchy folder_a -> folder_b -> folder_c -> file
// and folder_a is owned by user. Then user should have access to file.
Expand Down Expand Up @@ -133,19 +134,20 @@ func TestUsersetRewrites(t *testing.T) {
expectedPaths []path
}{{
// direct
query: "doc:document#owner@user",
query: "doc:document#owner@users:user",
expected: checkgroup.Result{
Membership: checkgroup.IsMember,
},
}, {
// userset rewrite
query: "doc:document#editor@user",
query: "doc:document#editor@users:user#...",
expected: checkgroup.Result{
Membership: checkgroup.IsMember,
},
}, {
// transitive userset rewrite
query: "doc:document#viewer@user",
query: "doc:document#viewer@users:user#",
// expected: checkgroup.ResultIsMember,
expected: checkgroup.Result{
Membership: checkgroup.IsMember,
},
Expand Down
20 changes: 8 additions & 12 deletions ketoapi/enc_string.go
Expand Up @@ -25,13 +25,7 @@ func (r *RelationTuple) String() string {
if r.SubjectID != nil {
sb.WriteString(*r.SubjectID)
} else if r.SubjectSet != nil {
sb.WriteRune('(')
sb.WriteString(r.SubjectSet.Namespace)
sb.WriteRune(':')
sb.WriteString(r.SubjectSet.Object)
sb.WriteRune('#')
sb.WriteString(r.SubjectSet.Relation)
sb.WriteRune(')')
sb.WriteString(r.SubjectSet.String())
} else {
sb.WriteString("<ERROR: no subject>")
}
Expand Down Expand Up @@ -59,7 +53,7 @@ func (r *RelationTuple) FromString(s string) (*RelationTuple, error) {

// remove optional brackets around the subject set
subject = strings.Trim(subject, "()")
if strings.Contains(subject, "#") {
if strings.Contains(subject, ":") {
subSet, err := (&SubjectSet{}).FromString(subject)
if err != nil {
return nil, err
Expand All @@ -73,14 +67,16 @@ func (r *RelationTuple) FromString(s string) (*RelationTuple, error) {
}

func (s *SubjectSet) String() string {
if s.Relation == "" {
return fmt.Sprintf("%s:%s", s.Namespace, s.Object)
}
return fmt.Sprintf("%s:%s#%s", s.Namespace, s.Object, s.Relation)
}

func (s *SubjectSet) FromString(str string) (*SubjectSet, error) {
namespaceAndObject, relation, ok := strings.Cut(str, "#")
if !ok {
return nil, errors.WithStack(ErrMalformedInput.WithDebug("expected subject set to contain '#'"))
}
// If there is no '#' we have a subject set without a relation, such as
// Users:Bob, which just means that the relation is empty.
namespaceAndObject, relation, _ := strings.Cut(str, "#")

namespace, object, ok := strings.Cut(namespaceAndObject, ":")
if !ok {
Expand Down
77 changes: 69 additions & 8 deletions ketoapi/enc_test.go
Expand Up @@ -16,16 +16,40 @@ import (

func TestRelationTuple(t *testing.T) {
t.Run("method=string encoding", func(t *testing.T) {
assert.Equal(t, "n:o#r@s", (&RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: x.Ptr("s"),
}).String())
for _, tc := range []struct {
tuple *RelationTuple
expected string
}{
{ // full tuple
tuple: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: x.Ptr("s"),
},
expected: "n:o#r@s",
},
{ // skip '#' in subject set when relation is empty
tuple: &RelationTuple{
Namespace: "groups",
Object: "dev",
Relation: "member",
SubjectSet: &SubjectSet{
Namespace: "users",
Object: "user",
},
},
expected: "groups:dev#member@users:user",
},
} {
t.Run("case="+tc.expected, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.tuple.String())
})
}
})

t.Run("method=string decoding", func(t *testing.T) {
for i, tc := range []struct {
for _, tc := range []struct {
enc string
err error
expected *RelationTuple
Expand All @@ -52,6 +76,43 @@ func TestRelationTuple(t *testing.T) {
},
},
},
{
enc: "n:o#r@n:o#...",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
Relation: "...",
},
},
},
{
enc: "n:o#r@n:o#",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
},
},
},
{
enc: "n:o#r@n:o",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
},
},
},
{
enc: "n:o#r@(n:o#r)",
expected: &RelationTuple{
Expand Down Expand Up @@ -91,7 +152,7 @@ func TestRelationTuple(t *testing.T) {
err: ErrMalformedInput,
},
} {
t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
t.Run(fmt.Sprintf("string=%s", tc.enc), func(t *testing.T) {
actual, err := (&RelationTuple{}).FromString(tc.enc)
assert.True(t, errors.Is(err, tc.err), "%+v", err)
assert.Equal(t, tc.expected, actual)
Expand Down

0 comments on commit ec979df

Please sign in to comment.