Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
402 lines
11 KiB
Go
402 lines
11 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/herodot"
|
|
|
|
"github.com/ory/keto/internal/expand"
|
|
"github.com/ory/keto/internal/namespace"
|
|
"github.com/ory/keto/ketoapi"
|
|
)
|
|
|
|
func runCases(c client, m *namespaceTestManager) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
c.waitUntilLive(t)
|
|
|
|
t.Run("case=list namespaces", func(t *testing.T) {
|
|
first := namespace.Namespace{Name: "my namespace"}
|
|
second := namespace.Namespace{Name: "my other namespace"}
|
|
m.add(t, &first, &second)
|
|
|
|
resp := c.queryNamespaces(t)
|
|
assert.GreaterOrEqual(t, len(resp.Namespaces), 2)
|
|
assertNamespacesContains(t, resp.Namespaces, "my namespace")
|
|
assertNamespacesContains(t, resp.Namespaces, "my other namespace")
|
|
})
|
|
|
|
t.Run("case=gets empty namespace", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
|
|
assert.Len(t, resp.RelationTuples, 0)
|
|
})
|
|
|
|
t.Run("case=creates tuple and uses it then", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
tuple := &ketoapi.RelationTuple{
|
|
Namespace: n.Name,
|
|
Object: fmt.Sprintf("object for client %T", c),
|
|
Relation: "access",
|
|
SubjectID: new("client"),
|
|
}
|
|
|
|
c.createTuple(t, tuple)
|
|
|
|
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &tuple.Namespace})
|
|
require.Len(t, resp.RelationTuples, 1)
|
|
assert.Equal(t, tuple, resp.RelationTuples[0])
|
|
|
|
// try the check API to see whether the tuple is interpreted correctly
|
|
assert.True(t, c.check(t, tuple))
|
|
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{tuple})
|
|
require.Len(t, batchResult, 1)
|
|
assert.True(t, batchResult[0].allowed)
|
|
assert.Empty(t, batchResult[0].errorMessage)
|
|
})
|
|
|
|
t.Run("case=creates tuple with empty IDs", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
tuples := []*ketoapi.RelationTuple{{
|
|
Namespace: n.Name,
|
|
Object: "",
|
|
Relation: "access",
|
|
SubjectID: new(""),
|
|
}, {
|
|
Namespace: n.Name,
|
|
Object: "",
|
|
Relation: "access",
|
|
SubjectSet: &ketoapi.SubjectSet{
|
|
Namespace: n.Name,
|
|
Object: "",
|
|
Relation: "access",
|
|
},
|
|
}}
|
|
|
|
for _, tp := range tuples {
|
|
c.createTuple(t, tp)
|
|
// try the check API to see whether the tuple is interpreted correctly
|
|
assert.True(t, c.check(t, tp))
|
|
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{tp})
|
|
require.Len(t, batchResult, 1)
|
|
assert.True(t, batchResult[0].allowed)
|
|
assert.Empty(t, batchResult[0].errorMessage)
|
|
}
|
|
|
|
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
|
|
assert.ElementsMatch(t, tuples, resp.RelationTuples)
|
|
})
|
|
|
|
t.Run("case=check subjectSet relations", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
obj := fmt.Sprintf("obj for client %T", c)
|
|
rel := "check"
|
|
|
|
rt := &ketoapi.RelationTuple{
|
|
Namespace: n.Name,
|
|
Object: obj,
|
|
Relation: rel,
|
|
SubjectSet: &ketoapi.SubjectSet{
|
|
Namespace: n.Name,
|
|
Object: obj,
|
|
Relation: rel,
|
|
},
|
|
}
|
|
c.createTuple(t, rt)
|
|
|
|
assert.True(t, c.check(t, rt))
|
|
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{rt})
|
|
require.Len(t, batchResult, 1)
|
|
assert.True(t, batchResult[0].allowed)
|
|
assert.Empty(t, batchResult[0].errorMessage)
|
|
})
|
|
|
|
t.Run("case=batch check", func(t *testing.T) {
|
|
/*
|
|
Test batch check with four cases:
|
|
- Allowed single subject tuple
|
|
- Allowed subject set tuple
|
|
- Tuple with unknown namespace (will return an error for this case)
|
|
- Dis-allowed tuple
|
|
*/
|
|
namespace1 := &namespace.Namespace{Name: t.Name()}
|
|
namespace2 := &namespace.Namespace{Name: t.Name() + "-2"}
|
|
m.add(t, namespace1)
|
|
m.add(t, namespace2)
|
|
|
|
obj1 := fmt.Sprintf("obj for client %T", c)
|
|
obj2 := fmt.Sprintf("another obj for client %T", c)
|
|
rel1 := "check"
|
|
rel2 := "access"
|
|
|
|
tupleSubjectSet := &ketoapi.RelationTuple{
|
|
Namespace: namespace1.Name,
|
|
Object: obj1,
|
|
Relation: rel1,
|
|
SubjectSet: &ketoapi.SubjectSet{
|
|
Namespace: namespace1.Name,
|
|
Object: obj1,
|
|
Relation: rel1,
|
|
},
|
|
}
|
|
c.createTuple(t, tupleSubjectSet)
|
|
|
|
tupleSingleSubject := &ketoapi.RelationTuple{
|
|
Namespace: namespace2.Name,
|
|
Object: obj2,
|
|
Relation: rel2,
|
|
SubjectID: new("sub1"),
|
|
}
|
|
c.createTuple(t, tupleSingleSubject)
|
|
|
|
unknownNamespaceTuple := &ketoapi.RelationTuple{
|
|
Namespace: "unknown-namespace",
|
|
Object: obj1,
|
|
Relation: rel1,
|
|
SubjectID: new("sub1"),
|
|
}
|
|
|
|
unknownSubjectTuple := &ketoapi.RelationTuple{
|
|
Namespace: namespace1.Name,
|
|
Object: obj1,
|
|
Relation: rel1,
|
|
SubjectID: new("unknown-sub"),
|
|
}
|
|
|
|
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{
|
|
tupleSubjectSet, tupleSingleSubject,
|
|
unknownNamespaceTuple, unknownSubjectTuple,
|
|
})
|
|
require.Len(t, batchResult, 4)
|
|
assert.True(t, batchResult[0].allowed)
|
|
assert.Empty(t, batchResult[0].errorMessage)
|
|
assert.True(t, batchResult[1].allowed)
|
|
assert.Empty(t, batchResult[1].errorMessage)
|
|
assert.False(t, batchResult[2].allowed)
|
|
assert.Contains(t, batchResult[2].errorMessage, "The requested resource could not be found")
|
|
assert.False(t, batchResult[3].allowed)
|
|
assert.Empty(t, batchResult[3].errorMessage)
|
|
|
|
// Verify a call with no tuples returns successfully with no results
|
|
emptyResults := c.batchCheck(t, []*ketoapi.RelationTuple{})
|
|
require.Empty(t, emptyResults)
|
|
})
|
|
|
|
t.Run("case=batch check validation errors", func(t *testing.T) {
|
|
// Pass in 11 tuples to check, more than the default limit of 10, and verify the request is rejected
|
|
tuples := make([]*ketoapi.RelationTuple, 11)
|
|
for i := range tuples {
|
|
tuples[i] = &ketoapi.RelationTuple{
|
|
Namespace: "namespace-name",
|
|
Object: "obj",
|
|
Relation: "rel",
|
|
SubjectID: new("sub"),
|
|
}
|
|
}
|
|
c.batchCheckErr(t, tuples, herodot.ErrBadRequest())
|
|
})
|
|
|
|
t.Run("case=expand API", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
obj := fmt.Sprintf("tree for client %T", c)
|
|
rel := "expand"
|
|
|
|
subjects := []string{"s1", "s2"}
|
|
expectedTree := &ketoapi.Tree[*ketoapi.RelationTuple]{
|
|
Type: ketoapi.TreeNodeUnion,
|
|
Tuple: &ketoapi.RelationTuple{
|
|
SubjectSet: &ketoapi.SubjectSet{
|
|
Namespace: n.Name,
|
|
Object: obj,
|
|
Relation: rel,
|
|
},
|
|
},
|
|
Children: make([]*ketoapi.Tree[*ketoapi.RelationTuple], len(subjects)),
|
|
}
|
|
|
|
for i, subjectID := range subjects {
|
|
c.createTuple(t, &ketoapi.RelationTuple{
|
|
Namespace: n.Name,
|
|
Object: obj,
|
|
Relation: rel,
|
|
SubjectID: &subjectID,
|
|
})
|
|
expectedTree.Children[i] = &ketoapi.Tree[*ketoapi.RelationTuple]{
|
|
Type: ketoapi.TreeNodeLeaf,
|
|
Tuple: &ketoapi.RelationTuple{
|
|
SubjectID: &subjectID,
|
|
},
|
|
}
|
|
}
|
|
|
|
actualTree := c.expand(t, expectedTree.Tuple.SubjectSet, 100)
|
|
|
|
assert.Equal(t, expectedTree.Type, actualTree.Type)
|
|
assert.Equal(t, expectedTree.Tuple, actualTree.Tuple)
|
|
assert.Len(t, actualTree.Children, len(expectedTree.Children))
|
|
expand.AssertExternalTreesAreEqual(t, expectedTree, actualTree)
|
|
})
|
|
|
|
t.Run("case=gets result paginated", func(t *testing.T) {
|
|
const nTuples = 10
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
rel := fmt.Sprintf("some unique relation %T", c)
|
|
for i := 0; i < nTuples; i++ {
|
|
c.createTuple(t, &ketoapi.RelationTuple{
|
|
Namespace: n.Name,
|
|
Object: "o" + strconv.Itoa(i),
|
|
Relation: rel,
|
|
SubjectID: new("s" + strconv.Itoa(i)),
|
|
})
|
|
}
|
|
|
|
var (
|
|
resp ketoapi.GetResponse
|
|
nPages int
|
|
)
|
|
// do ... while resp.NextPageToken != ""
|
|
for ok := true; ok; ok = resp.NextPageToken != "" {
|
|
resp = *c.queryTuple(t,
|
|
&ketoapi.RelationQuery{
|
|
Namespace: &n.Name,
|
|
Relation: &rel,
|
|
},
|
|
withToken(resp.NextPageToken),
|
|
withSize(1),
|
|
)
|
|
nPages++
|
|
assert.Len(t, resp.RelationTuples, 1)
|
|
}
|
|
|
|
assert.Equal(t, nTuples, nPages)
|
|
})
|
|
|
|
t.Run("case=deletes tuple", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
for _, rt := range []*ketoapi.RelationTuple{
|
|
{
|
|
SubjectID: new("s"),
|
|
},
|
|
{
|
|
SubjectSet: &ketoapi.SubjectSet{
|
|
Namespace: n.Name,
|
|
Object: "so",
|
|
Relation: "rel",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(fmt.Sprintf("subject_id=%v", rt.SubjectID == nil), func(t *testing.T) {
|
|
rt.Namespace = n.Name
|
|
rt.Object = "o"
|
|
rt.Relation = "rel"
|
|
c.createTuple(t, rt)
|
|
|
|
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
|
|
assert.Equal(t, []*ketoapi.RelationTuple{rt}, resp.RelationTuples)
|
|
|
|
c.deleteTuple(t, rt)
|
|
|
|
resp = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
|
|
assert.Len(t, resp.RelationTuples, 0)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("case=deletes tuples based on relation query", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
rts := []*ketoapi.RelationTuple{
|
|
{
|
|
Namespace: n.Name,
|
|
Object: "o1",
|
|
Relation: "rel",
|
|
SubjectID: new("s1"),
|
|
},
|
|
{
|
|
Namespace: n.Name,
|
|
Object: "o2",
|
|
Relation: "rel",
|
|
SubjectID: new("s2"),
|
|
},
|
|
}
|
|
for i := 0; i < len(rts); i++ {
|
|
c.createTuple(t, rts[i])
|
|
}
|
|
|
|
q := &ketoapi.RelationQuery{
|
|
Namespace: &n.Name,
|
|
Relation: new("rel"),
|
|
}
|
|
resp := c.queryTuple(t, q)
|
|
require.ElementsMatch(t, resp.RelationTuples, rts)
|
|
|
|
c.deleteAllTuples(t, q)
|
|
resp = c.queryTuple(t, q)
|
|
assert.Equal(t, 0, len(resp.RelationTuples))
|
|
})
|
|
|
|
t.Run("case=returns error with status code on unknown namespace", func(t *testing.T) {
|
|
c.queryTupleErr(t, herodot.ErrNotFound(), &ketoapi.RelationQuery{Namespace: new("unknown namespace")})
|
|
})
|
|
|
|
t.Run("case=still serves tuples from deleted namespace", func(t *testing.T) {
|
|
n := &namespace.Namespace{Name: t.Name()}
|
|
m.add(t, n)
|
|
|
|
tuple := &ketoapi.RelationTuple{
|
|
Namespace: n.Name,
|
|
Object: "o",
|
|
Relation: "rel",
|
|
SubjectID: new("s"),
|
|
}
|
|
c.createTuple(t, tuple)
|
|
|
|
m.remove(t, n.Name)
|
|
|
|
resp := c.queryTuple(t, &ketoapi.RelationQuery{})
|
|
assert.Equal(t, []*ketoapi.RelationTuple{tuple}, resp.RelationTuples)
|
|
|
|
// Add the namespace again here, so that we can clean up properly.
|
|
m.add(t, n)
|
|
})
|
|
|
|
t.Run("case=OPL syntax check", func(t *testing.T) {
|
|
parseErrors := c.oplCheckSyntax(t, []byte("/* unclosed comment"))
|
|
require.Len(t, parseErrors, 1)
|
|
assert.Contains(t, parseErrors[0].Message, "unclosed comment")
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertNamespacesContains(t *testing.T, list []ketoapi.Namespace, name string) {
|
|
t.Helper()
|
|
for _, n := range list {
|
|
if n.Name == name {
|
|
return
|
|
}
|
|
}
|
|
t.Errorf("Could not find %q in %+v", name, list)
|
|
}
|