-
-
Notifications
You must be signed in to change notification settings - Fork 347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: bulk deletion of relation tuples #799
feat: bulk deletion of relation tuples #799
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already a good start, but needs some improvements. Also, the same behavior should be reflected over gRPC as well.
} | ||
l.Debug("deleting relation tuples") | ||
|
||
rels, _, err := h.d.RelationTupleManager().GetRelationTuples(r.Context(), query) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite problematic as you only delete the first page here. Can you instead implement the selection of delete candidates on the persistence layer? A request DELETE /relation-tuples?namespace=foo
should result in the following SQL statement to be executed: DELETE FROM keto_relation_tuples WHERE namespace=foo
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, yeah I will try. For the gRPC part, I may need time to understand it because I've never worked with gRPC before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah so there is no specific delete RPC, but only the transact one. I would add a DeleteReleationTuples
RPC here
keto/proto/ory/keto/acl/v1alpha1/write_service.proto
Lines 14 to 20 in 67c4c60
// The write service to create and delete Access Control Lists. | |
// | |
// This service is part of the [write-APIs](../concepts/api-overview.mdx#write-apis). | |
service WriteService { | |
// Writes one or more relation tuples in a single transaction. | |
rpc TransactRelationTuples(TransactRelationTuplesRequest) returns (TransactRelationTuplesResponse); | |
} |
It should behave exactly as the REST handler, so you can probably use the same internal helper function. The current transaction handler is implemented here:
keto/internal/relationtuple/transact_server.go
Lines 30 to 53 in 67c4c60
func (h *handler) TransactRelationTuples(ctx context.Context, req *acl.TransactRelationTuplesRequest) (*acl.TransactRelationTuplesResponse, error) { | |
insertTuples, err := protoTuplesWithAction(req.RelationTupleDeltas, acl.RelationTupleDelta_INSERT) | |
if err != nil { | |
return nil, err | |
} | |
deleteTuples, err := protoTuplesWithAction(req.RelationTupleDeltas, acl.RelationTupleDelta_DELETE) | |
if err != nil { | |
return nil, err | |
} | |
err = h.d.RelationTupleManager().TransactRelationTuples(ctx, insertTuples, deleteTuples) | |
if err != nil { | |
return nil, err | |
} | |
snaptokens := make([]string, len(insertTuples)) | |
for i := range insertTuples { | |
snaptokens[i] = "not yet implemented" | |
} | |
return &acl.TransactRelationTuplesResponse{ | |
Snaptokens: snaptokens, | |
}, nil | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting there 😉
sqlRawQuery := "DELETE FROM keto_relation_tuples WHERE" | ||
args := []interface{}{} | ||
if query.Namespace != "" { | ||
n, err := p.GetNamespaceByName(ctx, query.Namespace) | ||
if err != nil { | ||
return err | ||
} | ||
sqlRawQuery += " namespace_id = ?" | ||
args = append(args, n.ID) | ||
} | ||
if query.Object != "" { | ||
sqlRawQuery += " AND object = ?" | ||
args = append(args, query.Object) | ||
} | ||
if query.Relation != "" { | ||
sqlRawQuery += " AND relation = ?" | ||
args = append(args, query.Relation) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the pop query builder for this instead of raw query. Just as we do here:
keto/internal/persistence/sql/relationtuples.go
Lines 218 to 236 in 7ee65b5
if query.Relation != "" { | |
sqlQuery.Where("relation = ?", query.Relation) | |
} | |
if query.Object != "" { | |
sqlQuery.Where("object = ?", query.Object) | |
} | |
if s := query.Subject(); s != nil { | |
if err := p.whereSubject(ctx, sqlQuery, s); err != nil { | |
return nil, "", err | |
} | |
} | |
if query.Namespace != "" { | |
n, err := p.GetNamespaceByName(ctx, query.Namespace) | |
if err != nil { | |
return nil, "", err | |
} | |
sqlQuery.Where("namespace_id = ?", n.ID) | |
} |
and here
keto/internal/persistence/sql/relationtuples.go
Lines 186 to 196 in 7ee65b5
q := p.QueryWithNetwork(ctx). | |
Where("namespace_id = ?", n.ID). | |
Where("object = ?", r.Object). | |
Where("relation = ?", r.Relation) | |
if err := p.whereSubject(ctx, q, r.Subject); err != nil { | |
return err | |
} | |
if err := q.Delete(&RelationTuple{}); err != nil { | |
return err | |
} |
As is, there is no SQL injection possible, but it is easy to add one later if you don't pay attention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my bad. I actually used pop query builder but for some reason, it didn't work. Now I realized why 🤪
I will push a new commit.
For the gRPC part, I had a basic understanding of it, now trying to figure out how to do it. |
@@ -52,6 +52,23 @@ func (h *handler) TransactRelationTuples(ctx context.Context, req *acl.TransactR | |||
}, nil | |||
} | |||
|
|||
func (h *handler) DeleteRelationTuples(ctx context.Context, req *acl.DeleteRelationTuplesRequest) (*acl.DeleteRelationTuplesResponse, error) { | |||
if req.Query == nil { | |||
return nil, errors.New("invalid request") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always return errors.WithStack(herodot.ErrInvalidRequest.WithReason(...))
or similar errors, they will automatically set the right response codes through the middle-ware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now this looks very good now. The very last thing would be to add e2e tests.
- here are all cases, add some more that test the new feature: https://github.com/ory/keto/blob/7ee65b5cd0827a3de87be3b6a7fa62d9038ea2df/internal/e2e/cases_test.go
- here is the interface definition for the clients, add a
deleteAll
function and implement it in all clients:keto/internal/e2e/full_suit_test.go
Lines 33 to 41 in 7ee65b5
client interface { createTuple(t require.TestingT, r *relationtuple.InternalRelationTuple) deleteTuple(t require.TestingT, r *relationtuple.InternalRelationTuple) queryTuple(t require.TestingT, q *relationtuple.RelationQuery, opts ...x.PaginationOptionSetter) *relationtuple.GetResponse queryTupleErr(t require.TestingT, expected herodot.DefaultError, q *relationtuple.RelationQuery, opts ...x.PaginationOptionSetter) check(t require.TestingT, r *relationtuple.InternalRelationTuple) bool expand(t require.TestingT, r *relationtuple.SubjectSet, depth int) *expand.Tree waitUntilLive(t require.TestingT) }
if query.Namespace != "" { | ||
n, err := p.GetNamespaceByName(ctx, query.Namespace) | ||
if err != nil { | ||
return err | ||
} | ||
sqlQuery.Where("namespace_id = ?", n.ID) | ||
} | ||
if query.Object != "" { | ||
sqlQuery.Where("object = ?", query.Object) | ||
} | ||
if query.Relation != "" { | ||
sqlQuery.Where("relation = ?", query.Relation) | ||
} | ||
if s := query.Subject(); s != nil { | ||
if err := p.whereSubject(ctx, sqlQuery, s); err != nil { | ||
return err | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now this looks way better, but it is the same code as in the GetRelationTuples
function.
Can you instead add a helper function similar to this (naming is just a draft):
// whereQuery takes a relation tuple query and appends where clauses to the base pop query
func whereQuery(ctx context.Context, baseQuery *pop.Query, rq *relationtuple.RelationQuery) (*pop.Query, error) {
if query.Namespace != "" {
n, err := p.GetNamespaceByName(ctx, rq.Namespace)
if err != nil {
return nil, err
}
baseQuery.Where("namespace_id = ?", n.ID)
}
if rq.Object != "" {
baseQuery.Where("object = ?", rq.Object)
}
if rq.Relation != "" {
baseQuery.Where("relation = ?", rq.Relation)
}
if s := rq.Subject(); s != nil {
if err := p.whereSubject(ctx, baseQuery, s); err != nil {
return err
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And then of course use it in both functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I will update it.
return nil, errors.New("invalid request") | ||
} | ||
|
||
q, err := (&RelationQuery{}).FromProto((*acl.ListRelationTuplesRequest_Query)(req.Query)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generated proto definitions have getter functions, so you can instead rewrite func (q *RelationQuery) FromProto(...)
to take an interface with the getter functions. That way you don't have to convert the type which might break in the future at some point.
I will add e2e tests later on. |
@zepatrik Currently I'm getting stuck in the CLI client test. The |
I think it would make sense to not break Lines 22 to 65 in fabf1a0
|
Does this mean that we will support two cases: one is deleting from JSON files and one is deleting by specifying query keys? |
@zepatrik How do you think about my latest change? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, this looks very good now 👍
I am really sorry for the late review.
@@ -152,11 +166,11 @@ func (h *handler) createRelation(w http.ResponseWriter, r *http.Request, _ httpr | |||
h.d.Writer().WriteCreated(w, r, RouteBase+"?"+q.Encode(), &rel) | |||
} | |||
|
|||
// swagger:route DELETE /relation-tuples write deleteRelationTuple | |||
// swagger:route DELETE /relation-tuples write deleteRelationTuples |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will result in breaking changes in the SDK, right @aeneasr? The behavior changed a bit, but nothing that should break existing integrations.
// Deletes relation tuples based on relation query | ||
rpc DeleteRelationTuples(DeleteRelationTuplesRequest) returns (DeleteRelationTuplesResponse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, as this is a new rpc it will not break existing integrations.
I actually just realized that splitting the |
Related issue(s)
Closes #599
Checklist
introduces a new feature.
contributing code guidelines.
vulnerability. If this pull request addresses a security. vulnerability, I
confirm that I got green light (please contact
security@ory.sh) from the maintainers to push
the changes.
works.
Further Comments