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
321 lines
8.8 KiB
Go
321 lines
8.8 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package relationtuple
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/pkg/errors"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/ory/herodot"
|
|
|
|
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
|
"github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect"
|
|
"github.com/ory/keto/internal/x/validate"
|
|
"github.com/ory/keto/ketoapi"
|
|
"github.com/ory/keto/x/events"
|
|
)
|
|
|
|
var _ relationtuplesconnect.WriteServiceHandler = (*WriteHandler)(nil)
|
|
|
|
func protoTuplesWithAction(deltas []*rts.RelationTupleDelta, action rts.RelationTupleDelta_Action) (filtered []*ketoapi.RelationTuple, err error) {
|
|
for _, d := range deltas {
|
|
if d.Action == action {
|
|
it, err := (&ketoapi.RelationTuple{}).FromDataProvider(d.RelationTuple)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filtered = append(filtered, it)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (h *WriteHandler) TransactRelationTuples(ctx context.Context, req *connect.Request[rts.TransactRelationTuplesRequest]) (*connect.Response[rts.TransactRelationTuplesResponse], error) {
|
|
insertTuples, err := protoTuplesWithAction(req.Msg.RelationTupleDeltas, rts.RelationTupleDelta_ACTION_INSERT)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deleteTuples, err := protoTuplesWithAction(req.Msg.RelationTupleDeltas, rts.RelationTupleDelta_ACTION_DELETE)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = h.d.RelationTupleManager().Transaction(ctx, func(ctx context.Context) error {
|
|
its, err := h.d.Mapper().FromTuple(ctx, append(insertTuples, deleteTuples...)...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return h.d.RelationTupleManager().TransactRelationTuples(ctx, its[:len(insertTuples)], its[len(insertTuples):])
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesChanged(ctx))
|
|
|
|
snaptokens := make([]string, len(insertTuples))
|
|
for i := range insertTuples {
|
|
snaptokens[i] = "not yet implemented"
|
|
}
|
|
return connect.NewResponse(&rts.TransactRelationTuplesResponse{
|
|
Snaptokens: snaptokens,
|
|
}), nil
|
|
}
|
|
|
|
func (h *WriteHandler) DeleteRelationTuples(ctx context.Context, req *connect.Request[rts.DeleteRelationTuplesRequest]) (*connect.Response[rts.DeleteRelationTuplesResponse], error) {
|
|
var q ketoapi.RelationQuery
|
|
|
|
switch {
|
|
case req.Msg.RelationQuery != nil:
|
|
q.FromDataProvider(&queryWrapper{req.Msg.RelationQuery})
|
|
//lint:ignore SA1019 required for compatibility
|
|
case req.Msg.Query != nil: //nolint:staticcheck
|
|
//lint:ignore SA1019 backwards compatibility
|
|
q.FromDataProvider(&deprecatedQueryWrapper{(*rts.ListRelationTuplesRequest_Query)(req.Msg.Query)}) //nolint:staticcheck
|
|
default:
|
|
return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("invalid request"))
|
|
}
|
|
|
|
iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, &q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := h.d.RelationTupleManager().DeleteAllRelationTuples(ctx, iq); err != nil {
|
|
return nil, errors.WithStack(herodot.ErrInternalServerError().WithError(err.Error()))
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesDeleted(ctx))
|
|
|
|
return connect.NewResponse(&rts.DeleteRelationTuplesResponse{}), nil
|
|
}
|
|
|
|
// Create Relationship Request Parameters
|
|
//
|
|
// swagger:parameters createRelationship
|
|
//
|
|
//lint:ignore U1000 required for OpenAPI
|
|
type createRelationship struct {
|
|
// in: body
|
|
Body createRelationshipBody
|
|
}
|
|
|
|
// Create Relationship Request Body
|
|
//
|
|
// swagger:model createRelationshipBody
|
|
type createRelationshipBody struct {
|
|
ketoapi.RelationQuery
|
|
}
|
|
|
|
// swagger:route PUT /admin/relation-tuples relationship createRelationship
|
|
//
|
|
// # Create a Relationship
|
|
//
|
|
// Use this endpoint to create a relationship.
|
|
//
|
|
// Consumes:
|
|
// - application/json
|
|
//
|
|
// Produces:
|
|
// - application/json
|
|
//
|
|
// Schemes: http, https
|
|
//
|
|
// Responses:
|
|
// 201: relationship
|
|
// 400: errorGeneric
|
|
// default: errorGeneric
|
|
//
|
|
// Extensions:
|
|
// x-ory-ratelimit-bucket: keto-admin-low
|
|
func (h *WriteHandler) createRelation(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
var rt ketoapi.RelationTuple
|
|
if err := json.NewDecoder(r.Body).Decode(&rt); err != nil {
|
|
h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error())))
|
|
return
|
|
}
|
|
|
|
if err := rt.Validate(); err != nil {
|
|
h.d.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
|
|
h.d.Logger().WithFields(rt.ToLoggerFields()).Debug("creating relation tuple")
|
|
|
|
err := h.d.RelationTupleManager().Transaction(ctx, func(ctx context.Context) error {
|
|
it, err := h.d.Mapper().FromTuple(ctx, &rt)
|
|
if err != nil {
|
|
h.d.Logger().WithError(err).WithFields(rt.ToLoggerFields()).Errorf("could not map relation tuple to UUIDs")
|
|
return err
|
|
}
|
|
if err := h.d.RelationTupleManager().WriteRelationTuples(ctx, it...); err != nil {
|
|
h.d.Logger().WithError(err).WithFields(rt.ToLoggerFields()).Errorf("got an error while creating the relation tuple")
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
h.d.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesCreated(ctx))
|
|
|
|
h.d.Writer().WriteCreated(w, r,
|
|
ReadRouteBase+"?"+rt.ToURLQuery().Encode(),
|
|
&rt,
|
|
)
|
|
}
|
|
|
|
// swagger:route DELETE /admin/relation-tuples relationship deleteRelationships
|
|
//
|
|
// # Delete Relationships
|
|
//
|
|
// Use this endpoint to delete relationships
|
|
//
|
|
// Consumes:
|
|
// - application/x-www-form-urlencoded
|
|
//
|
|
// Produces:
|
|
// - application/json
|
|
//
|
|
// Schemes: http, https
|
|
//
|
|
// Responses:
|
|
// 204: emptyResponse
|
|
// 400: errorGeneric
|
|
// default: errorGeneric
|
|
//
|
|
// Extensions:
|
|
// x-ory-ratelimit-bucket: keto-admin-low
|
|
func (h *WriteHandler) deleteRelations(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
if err := validate.All(r,
|
|
validate.NoExtraQueryParams(ketoapi.RelationQueryKeys...),
|
|
validate.QueryParamsContainsOneOf(ketoapi.NamespaceKey),
|
|
validate.HasEmptyBody(),
|
|
); err != nil {
|
|
h.d.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
|
|
q := r.URL.Query()
|
|
query, err := (&ketoapi.RelationQuery{}).FromURLQuery(q)
|
|
if err != nil {
|
|
h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()))
|
|
return
|
|
}
|
|
|
|
l := h.d.Logger()
|
|
for k := range q {
|
|
l = l.WithField(k, q.Get(k))
|
|
}
|
|
l.Debug("deleting relationships")
|
|
|
|
iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, query)
|
|
if err != nil {
|
|
h.d.Logger().WithError(err).Errorf("could not map fields to UUIDs")
|
|
h.d.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
if err := h.d.RelationTupleManager().DeleteAllRelationTuples(ctx, iq); err != nil {
|
|
l.WithError(err).Errorf("got an error while deleting relationships")
|
|
h.d.Writer().WriteError(w, r, herodot.ErrInternalServerError().WithError(err.Error()))
|
|
return
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesDeleted(ctx))
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func internalTuplesWithAction(deltas []*ketoapi.PatchDelta, action ketoapi.PatchAction) (filtered []*ketoapi.RelationTuple) {
|
|
for _, d := range deltas {
|
|
if d.Action == action {
|
|
filtered = append(filtered, d.RelationTuple)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// swagger:route PATCH /admin/relation-tuples relationship patchRelationships
|
|
//
|
|
// # Patch Multiple Relationships
|
|
//
|
|
// Use this endpoint to patch one or more relationships.
|
|
//
|
|
// Consumes:
|
|
// - application/json
|
|
//
|
|
// Produces:
|
|
// - application/json
|
|
//
|
|
// Schemes: http, https
|
|
//
|
|
// Responses:
|
|
// 204: emptyResponse
|
|
// 400: errorGeneric
|
|
// 404: errorGeneric
|
|
// default: errorGeneric
|
|
//
|
|
// Extensions:
|
|
// x-ory-ratelimit-bucket: keto-admin-low
|
|
func (h *WriteHandler) patchRelationTuples(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
var deltas []*ketoapi.PatchDelta
|
|
if err := json.NewDecoder(r.Body).Decode(&deltas); err != nil {
|
|
h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()))
|
|
return
|
|
}
|
|
for _, d := range deltas {
|
|
if d.RelationTuple == nil {
|
|
h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("relation_tuple is missing"))
|
|
return
|
|
}
|
|
if err := d.RelationTuple.Validate(); err != nil {
|
|
h.d.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
|
|
switch d.Action {
|
|
case ketoapi.ActionInsert, ketoapi.ActionDelete:
|
|
default:
|
|
h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("unknown action "+string(d.Action)))
|
|
return
|
|
}
|
|
}
|
|
|
|
insertTuples := internalTuplesWithAction(deltas, ketoapi.ActionInsert)
|
|
deleteTuples := internalTuplesWithAction(deltas, ketoapi.ActionDelete)
|
|
|
|
err := h.d.RelationTupleManager().Transaction(ctx, func(ctx context.Context) error {
|
|
its, err := h.d.Mapper().FromTuple(ctx, append(insertTuples, deleteTuples...)...)
|
|
if err != nil {
|
|
h.d.Logger().WithError(err).Errorf("got an error while mapping fields to UUID")
|
|
return err
|
|
}
|
|
return h.d.RelationTupleManager().TransactRelationTuples(ctx, its[:len(insertTuples)], its[len(insertTuples):])
|
|
})
|
|
|
|
if err != nil {
|
|
h.d.Writer().WriteError(w, r, err)
|
|
return
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesChanged(ctx))
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|