Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions internal/e2e/testcases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,28 @@ func runCases(c client, m *namespaceTestManager) func(*testing.T) {
m.add(t, n)
})

t.Run("case=deletes leftover tuples after namespace is removed", 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)

c.deleteAllTuples(t, &ketoapi.RelationQuery{Namespace: &n.Name})

// Re-add the namespace so the list API can confirm the leftovers are gone.
m.add(t, n)
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
assert.Len(t, resp.RelationTuples, 0)
})

t.Run("case=OPL syntax check", func(t *testing.T) {
parseErrors := c.oplCheckSyntax(t, []byte("/* unclosed comment"))
require.Len(t, parseErrors, 1)
Expand Down
4 changes: 2 additions & 2 deletions internal/relationtuple/transact_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (h *WriteHandler) DeleteRelationTuples(ctx context.Context, req *connect.Re
return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("invalid request"))
}

iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, &q)
iq, err := h.d.ReadOnlyMapper().FromDeleteQuery(ctx, &q)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -223,7 +223,7 @@ func (h *WriteHandler) deleteRelations(w http.ResponseWriter, r *http.Request) {
}
l.Debug("deleting relationships")

iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, query)
iq, err := h.d.ReadOnlyMapper().FromDeleteQuery(ctx, query)
if err != nil {
h.d.Logger().WithError(err).Errorf("could not map fields to UUIDs")
h.d.Writer().WriteError(w, r, err)
Expand Down
31 changes: 31 additions & 0 deletions internal/relationtuple/transact_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,37 @@ func TestWriteHandlers(t *testing.T) {
assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs)
})

t.Run("case=deletes leftover tuples after namespace is removed", func(t *testing.T) {
nspace := addNamespace(t)

rt := &ketoapi.RelationTuple{
Namespace: nspace.Name,
Object: "leftover obj",
Relation: "leftover rel",
SubjectID: new("leftover subj"),
}
testhelpers.MapAndInsertTuples(t, reg, rt)

remaining := make([]*namespace.Namespace, 0, len(nspaces))
for _, n := range nspaces {
if n.Name != nspace.Name {
remaining = append(remaining, n)
}
}
nspaces = remaining
require.NoError(t, reg.Config(t.Context()).Set(config.KeyNamespaces, nspaces))

req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.WriteRouteBase+"?"+rt.ToURLQuery().Encode(), nil)
require.NoError(t, err)
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
require.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)

actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace.Name}, keysetpagination.WithSize(10))
require.NoError(t, err)
assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs)
})

t.Run("case=deletes multiple tuples", func(t *testing.T) {
nspace := addNamespace(t)

Expand Down
33 changes: 29 additions & 4 deletions internal/relationtuple/uuid_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ package relationtuple
import (
"context"

"github.com/ory/herodot"
"github.com/ory/x/otelx"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/trace"

"github.com/ory/keto/internal/driver/config"
"github.com/ory/keto/internal/namespace"
"github.com/ory/keto/ketoapi"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -65,7 +68,29 @@ func (c *success) apply() {
func (m *Mapper) FromQuery(ctx context.Context, apiQuery *ketoapi.RelationQuery) (res *RelationQuery, err error) {
ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.FromQuery")
defer otelx.End(span, &err)
return m.fromQuery(ctx, apiQuery, false)
}

// FromDeleteQuery maps a delete query. Unlike FromQuery, a missing namespace
// definition is not an error so leftover relationships can still be removed.
func (m *Mapper) FromDeleteQuery(ctx context.Context, apiQuery *ketoapi.RelationQuery) (res *RelationQuery, err error) {
ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.FromDeleteQuery")
defer otelx.End(span, &err)
return m.fromQuery(ctx, apiQuery, true)
}

func resolveNamespaceName(ctx context.Context, nm namespace.Manager, name string, allowUnknown bool) (string, error) {
n, err := nm.GetNamespaceByName(ctx, name)
if err != nil {
if allowUnknown && errors.Is(err, herodot.ErrNotFound()) {
return name, nil
}
return "", err
}
return n.Name, nil
}

func (m *Mapper) fromQuery(ctx context.Context, apiQuery *ketoapi.RelationQuery, allowUnknownNamespace bool) (res *RelationQuery, err error) {
onSuccess := newSuccess(&err)
defer onSuccess.apply()

Expand All @@ -81,11 +106,11 @@ func (m *Mapper) FromQuery(ctx context.Context, apiQuery *ketoapi.RelationQuery)
}

if apiQuery.Namespace != nil {
n, err := nm.GetNamespaceByName(ctx, *apiQuery.Namespace)
name, err := resolveNamespaceName(ctx, nm, *apiQuery.Namespace, allowUnknownNamespace)
if err != nil {
return nil, err
}
res.Namespace = new(n.Name)
res.Namespace = new(name)
}
if apiQuery.Object != nil {
s = append(s, *apiQuery.Object)
Expand All @@ -105,14 +130,14 @@ func (m *Mapper) FromQuery(ctx context.Context, apiQuery *ketoapi.RelationQuery)
}
if apiQuery.SubjectSet != nil {
s = append(s, apiQuery.SubjectSet.Object)
n, err := nm.GetNamespaceByName(ctx, apiQuery.SubjectSet.Namespace)
name, err := resolveNamespaceName(ctx, nm, apiQuery.SubjectSet.Namespace, allowUnknownNamespace)
if err != nil {
return nil, err
}
onSuccess.do(func(i int) func() {
return func() {
res.Subject = &SubjectSet{
Namespace: n.Name,
Namespace: name,
Object: u[i],
Relation: apiQuery.SubjectSet.Relation,
}
Expand Down
14 changes: 14 additions & 0 deletions internal/relationtuple/uuid_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,20 @@ func TestMapper(t *testing.T) {
})
})
}

t.Run("unknown namespace is allowed for delete", func(t *testing.T) {
runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) {
query := &ketoapi.RelationQuery{
Namespace: new("unknown"),
Object: new("object"),
}
mapped, err := m.FromDeleteQuery(ctx, query)
require.NoError(t, err)
require.NotNil(t, mapped.Namespace)
assert.Equal(t, "unknown", *mapped.Namespace)
require.NotNil(t, mapped.Object)
})
})
})

t.Run("item=subject set", func(t *testing.T) {
Expand Down
Loading