snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
526
internal/check/handler.go
Normal file
526
internal/check/handler.go
Normal file
@@ -0,0 +1,526 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/x/httprouterx"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
|
||||
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/driver/config"
|
||||
"github.com/ory/keto/internal/relationtuple"
|
||||
"github.com/ory/keto/internal/x"
|
||||
"github.com/ory/keto/ketoapi"
|
||||
)
|
||||
|
||||
type (
|
||||
handlerDependencies interface {
|
||||
EngineProvider
|
||||
relationtuple.ManagerProvider
|
||||
relationtuple.MapperProvider
|
||||
logrusx.Provider
|
||||
httpx.WriterProvider
|
||||
config.Provider
|
||||
x.HandlerOptionsProvider
|
||||
}
|
||||
Handler struct {
|
||||
relationtuplesconnect.UnimplementedCheckServiceHandler
|
||||
d handlerDependencies
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
_ relationtuplesconnect.CheckServiceHandler = (*Handler)(nil)
|
||||
_ *checkPermission = nil
|
||||
)
|
||||
|
||||
func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} }
|
||||
|
||||
const (
|
||||
RouteBase = "/relation-tuples/check"
|
||||
OpenAPIRouteBase = RouteBase + "/openapi"
|
||||
BatchRoute = "/relation-tuples/batch/check"
|
||||
)
|
||||
|
||||
func (h *Handler) RegisterReadRoutes(r *httprouterx.RouterPublic) {
|
||||
r.GET(RouteBase, h.getCheckMirrorStatus)
|
||||
r.GET(OpenAPIRouteBase, h.getCheckNoStatus)
|
||||
r.POST(RouteBase, h.postCheckMirrorStatus)
|
||||
r.POST(OpenAPIRouteBase, h.postCheckNoStatus)
|
||||
r.POST(BatchRoute, h.batchCheck)
|
||||
|
||||
checkServiceMethods := rts.File_ory_keto_relation_tuples_v1alpha2_check_service_proto.Services().ByName("CheckService").Methods()
|
||||
|
||||
checkHandler := connect.NewUnaryHandler(
|
||||
relationtuplesconnect.CheckServiceCheckProcedure,
|
||||
h.Check,
|
||||
connect.WithSchema(checkServiceMethods.ByName("Check")),
|
||||
connect.WithHandlerOptions(h.d.HandlerOptions()...),
|
||||
)
|
||||
r.Handle(relationtuplesconnect.CheckServiceCheckProcedure, checkHandler)
|
||||
|
||||
batchCheckHandler := connect.NewUnaryHandler(
|
||||
relationtuplesconnect.CheckServiceBatchCheckProcedure,
|
||||
h.BatchCheck,
|
||||
connect.WithSchema(checkServiceMethods.ByName("BatchCheck")),
|
||||
connect.WithHandlerOptions(h.d.HandlerOptions()...),
|
||||
)
|
||||
r.Handle(relationtuplesconnect.CheckServiceBatchCheckProcedure, batchCheckHandler)
|
||||
}
|
||||
|
||||
func (h *Handler) ProtoFiles() []protoreflect.FileDescriptor {
|
||||
return []protoreflect.FileDescriptor{
|
||||
rts.File_ory_keto_relation_tuples_v1alpha2_check_service_proto,
|
||||
rts.File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto,
|
||||
}
|
||||
}
|
||||
|
||||
// Check Permission Result
|
||||
//
|
||||
// The content of the allowed field is mirrored in the HTTP status code.
|
||||
//
|
||||
// swagger:model checkPermissionResult
|
||||
type CheckPermissionResult struct {
|
||||
// whether the relation tuple is allowed
|
||||
//
|
||||
// required: true
|
||||
Allowed bool `json:"allowed"`
|
||||
}
|
||||
|
||||
// Check Permission Result With Error
|
||||
//
|
||||
// swagger:model checkPermissionResultWithError
|
||||
type CheckPermissionResultWithError struct {
|
||||
// whether the relation tuple is allowed
|
||||
//
|
||||
// required: true
|
||||
Allowed bool `json:"allowed"`
|
||||
// any error generated while checking the relation tuple
|
||||
//
|
||||
// required: false
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Check Permission Request Parameters
|
||||
//
|
||||
// swagger:parameters checkPermission
|
||||
type checkPermission struct {
|
||||
// in: query
|
||||
MaxDepth int `json:"max-depth"`
|
||||
}
|
||||
|
||||
// swagger:route GET /relation-tuples/check/openapi permission checkPermission
|
||||
//
|
||||
// # Check a permission
|
||||
//
|
||||
// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).
|
||||
//
|
||||
// Consumes:
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: checkPermissionResult
|
||||
// 400: errorGeneric
|
||||
// default: errorGeneric
|
||||
//
|
||||
// Extensions:
|
||||
// x-ory-ratelimit-bucket: keto-public-high
|
||||
func (h *Handler) getCheckNoStatus(w http.ResponseWriter, r *http.Request) {
|
||||
allowed, err := h.getCheck(r.Context(), r.URL.Query())
|
||||
if err != nil {
|
||||
h.d.Writer().WriteError(w, r, err)
|
||||
return
|
||||
}
|
||||
h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed})
|
||||
}
|
||||
|
||||
// Check Permission Or Error Request Parameters
|
||||
//
|
||||
// swagger:parameters checkPermissionOrError
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type checkPermissionOrError struct {
|
||||
// in: query
|
||||
MaxDepth int `json:"max-depth"`
|
||||
}
|
||||
|
||||
// swagger:route GET /relation-tuples/check permission checkPermissionOrError
|
||||
//
|
||||
// # Check a permission
|
||||
//
|
||||
// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).
|
||||
//
|
||||
// Consumes:
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: checkPermissionResult
|
||||
// 400: errorGeneric
|
||||
// 403: checkPermissionResult
|
||||
// default: errorGeneric
|
||||
//
|
||||
// Extensions:
|
||||
// x-ory-ratelimit-bucket: keto-public-high
|
||||
func (h *Handler) getCheckMirrorStatus(w http.ResponseWriter, r *http.Request) {
|
||||
allowed, err := h.getCheck(r.Context(), r.URL.Query())
|
||||
if err != nil {
|
||||
h.d.Writer().WriteError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if allowed {
|
||||
h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed})
|
||||
return
|
||||
}
|
||||
|
||||
h.d.Writer().WriteCode(w, r, http.StatusForbidden, &CheckPermissionResult{Allowed: allowed})
|
||||
}
|
||||
|
||||
func (h *Handler) getCheck(ctx context.Context, q url.Values) (bool, error) {
|
||||
maxDepth, err := x.GetMaxDepthFromQuery(q)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
tuple, err := (&ketoapi.RelationTuple{}).FromURLQuery(q)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
it, err := h.d.ReadOnlyMapper().FromTuple(ctx, tuple)
|
||||
// herodot.ErrNotFound occurs when the namespace is unknown
|
||||
if errors.Is(err, herodot.ErrNotFound()) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return h.d.PermissionEngine().CheckIsMember(ctx, it[0], maxDepth)
|
||||
}
|
||||
|
||||
// Check Permission using Post Request Parameters
|
||||
//
|
||||
// swagger:parameters postCheckPermission
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type postCheckPermission struct {
|
||||
// in: query
|
||||
MaxDepth int `json:"max-depth"`
|
||||
|
||||
// in: body
|
||||
Payload postCheckPermissionBody
|
||||
}
|
||||
|
||||
// Check Permission using Post Request Body
|
||||
//
|
||||
// swagger:model postCheckPermissionBody
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type postCheckPermissionBody struct {
|
||||
ketoapi.RelationQuery
|
||||
}
|
||||
|
||||
// swagger:route POST /relation-tuples/check/openapi permission postCheckPermission
|
||||
//
|
||||
// # Check a permission
|
||||
//
|
||||
// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: checkPermissionResult
|
||||
// 400: errorGeneric
|
||||
// default: errorGeneric
|
||||
//
|
||||
// Extensions:
|
||||
// x-ory-ratelimit-bucket: keto-public-high
|
||||
func (h *Handler) postCheckNoStatus(w http.ResponseWriter, r *http.Request) {
|
||||
allowed, err := h.postCheck(r.Context(), r.Body, r.URL.Query())
|
||||
if err != nil {
|
||||
h.d.Writer().WriteError(w, r, err)
|
||||
return
|
||||
}
|
||||
h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed})
|
||||
}
|
||||
|
||||
// Post Check Permission Or Error Request Parameters
|
||||
//
|
||||
// swagger:parameters postCheckPermissionOrError
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type postCheckPermissionOrError struct {
|
||||
// in: query
|
||||
MaxDepth int `json:"max-depth"`
|
||||
|
||||
// in: body
|
||||
Body postCheckPermissionOrErrorBody
|
||||
}
|
||||
|
||||
// Post Check Permission Or Error Body
|
||||
//
|
||||
// swagger:model postCheckPermissionOrErrorBody
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type postCheckPermissionOrErrorBody struct {
|
||||
ketoapi.RelationQuery
|
||||
}
|
||||
|
||||
// swagger:route POST /relation-tuples/check permission postCheckPermissionOrError
|
||||
//
|
||||
// # Check a permission
|
||||
//
|
||||
// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: checkPermissionResult
|
||||
// 400: errorGeneric
|
||||
// 403: checkPermissionResult
|
||||
// default: errorGeneric
|
||||
//
|
||||
// Extensions:
|
||||
// x-ory-ratelimit-bucket: keto-public-high
|
||||
func (h *Handler) postCheckMirrorStatus(w http.ResponseWriter, r *http.Request) {
|
||||
allowed, err := h.postCheck(r.Context(), r.Body, r.URL.Query())
|
||||
if err != nil {
|
||||
h.d.Writer().WriteError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if allowed {
|
||||
h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed})
|
||||
return
|
||||
}
|
||||
|
||||
h.d.Writer().WriteCode(w, r, http.StatusForbidden, &CheckPermissionResult{Allowed: allowed})
|
||||
}
|
||||
|
||||
func (h *Handler) postCheck(ctx context.Context, body io.Reader, query url.Values) (bool, error) {
|
||||
maxDepth, err := x.GetMaxDepthFromQuery(query)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var tuple ketoapi.RelationTuple
|
||||
if err := json.NewDecoder(body).Decode(&tuple); err != nil {
|
||||
return false, errors.WithStack(herodot.ErrBadRequest().WithErrorf("could not unmarshal json: %s", err.Error()))
|
||||
}
|
||||
t, err := h.d.ReadOnlyMapper().FromTuple(ctx, &tuple)
|
||||
// herodot.ErrNotFound occurs when the namespace is unknown
|
||||
if errors.Is(err, herodot.ErrNotFound()) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return h.d.PermissionEngine().CheckIsMember(ctx, t[0], maxDepth)
|
||||
}
|
||||
|
||||
func (h *Handler) Check(ctx context.Context, req *connect.Request[rts.CheckRequest]) (*connect.Response[rts.CheckResponse], error) {
|
||||
var src ketoapi.TupleData
|
||||
if req.Msg.Tuple != nil {
|
||||
src = req.Msg.Tuple
|
||||
} else {
|
||||
src = req.Msg
|
||||
}
|
||||
|
||||
tuple, err := (&ketoapi.RelationTuple{}).FromDataProvider(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
internalTuple, err := h.d.ReadOnlyMapper().FromTuple(ctx, tuple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowed, err := h.d.PermissionEngine().CheckIsMember(ctx, internalTuple[0], int(req.Msg.MaxDepth))
|
||||
// TODO add content change handling
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connect.NewResponse(&rts.CheckResponse{ //nolint:gosec // Snaptoken is not a credential, just a placeholder value.
|
||||
Allowed: allowed,
|
||||
Snaptoken: "not yet implemented",
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Batch Check Permission Request Parameters
|
||||
//
|
||||
// swagger:parameters batchCheckPermission
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type batchCheckPermission struct {
|
||||
// in: query
|
||||
MaxDepth int `json:"max-depth"`
|
||||
|
||||
// in: body
|
||||
Body batchCheckPermissionBody
|
||||
}
|
||||
|
||||
// Batch Check Permission Body
|
||||
//
|
||||
// swagger:model batchCheckPermissionBody
|
||||
//
|
||||
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
|
||||
type batchCheckPermissionBody struct {
|
||||
Tuples []*ketoapi.RelationTuple `json:"tuples"`
|
||||
}
|
||||
|
||||
// Batch Check Permission Result
|
||||
//
|
||||
// swagger:model batchCheckPermissionResult
|
||||
type BatchCheckPermissionResult struct {
|
||||
// An array of check results. The order aligns with the input order.
|
||||
//
|
||||
// required: true
|
||||
Results []*CheckPermissionResultWithError `json:"results"`
|
||||
}
|
||||
|
||||
// swagger:route POST /relation-tuples/batch/check permission batchCheckPermission
|
||||
//
|
||||
// # Batch check permissions
|
||||
//
|
||||
// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: http, https
|
||||
//
|
||||
// Responses:
|
||||
// 200: batchCheckPermissionResult
|
||||
// 400: errorGeneric
|
||||
// default: errorGeneric
|
||||
//
|
||||
// Extensions:
|
||||
// x-ory-ratelimit-bucket: keto-public-high
|
||||
func (h *Handler) batchCheck(w http.ResponseWriter, r *http.Request) {
|
||||
results, err := h.doBatchCheck(r.Context(), r.Body, r.URL.Query())
|
||||
if err != nil {
|
||||
h.d.Writer().WriteError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.d.Writer().Write(w, r, &BatchCheckPermissionResult{Results: results})
|
||||
}
|
||||
|
||||
// doBatchCheck is the HTTP entry point for checking batches of tuples
|
||||
func (h *Handler) doBatchCheck(ctx context.Context, body io.Reader, query url.Values) ([]*CheckPermissionResultWithError, error) {
|
||||
maxDepth, err := x.GetMaxDepthFromQuery(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var request batchCheckPermissionBody
|
||||
if err := json.NewDecoder(body).Decode(&request); err != nil {
|
||||
return nil, errors.WithStack(herodot.ErrBadRequest().WithErrorf("could not unmarshal json: %s", err.Error()))
|
||||
}
|
||||
|
||||
if len(request.Tuples) > h.d.Config(ctx).BatchCheckMaxBatchSize() {
|
||||
return nil, herodot.ErrBadRequest().WithErrorf("batch exceeds max size of %v",
|
||||
h.d.Config(ctx).BatchCheckMaxBatchSize())
|
||||
}
|
||||
|
||||
results, err := h.d.PermissionEngine().BatchCheck(ctx, request.Tuples, maxDepth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
strict := h.d.Config(ctx).StrictMode()
|
||||
responses := make([]*CheckPermissionResultWithError, len(request.Tuples))
|
||||
for i, result := range results {
|
||||
responses[i] = &CheckPermissionResultWithError{
|
||||
Allowed: result.Membership == IsMember,
|
||||
Error: batchResultErrMsg(result, strict),
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// BatchCheck is the gRPC entry point for checking batches of tuples
|
||||
func (h *Handler) BatchCheck(ctx context.Context, req *connect.Request[rts.BatchCheckRequest]) (*connect.Response[rts.BatchCheckResponse], error) {
|
||||
if len(req.Msg.Tuples) > h.d.Config(ctx).BatchCheckMaxBatchSize() {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf(
|
||||
"batch exceeds max size of %v", h.d.Config(ctx).BatchCheckMaxBatchSize()))
|
||||
}
|
||||
|
||||
ketoTuples := make([]*ketoapi.RelationTuple, len(req.Msg.Tuples))
|
||||
for i, tuple := range req.Msg.Tuples {
|
||||
ketoTuples[i] = (&ketoapi.RelationTuple{}).FromProto(tuple)
|
||||
}
|
||||
|
||||
results, err := h.d.PermissionEngine().BatchCheck(ctx, ketoTuples, int(req.Msg.MaxDepth))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
strict := h.d.Config(ctx).StrictMode()
|
||||
responses := make([]*rts.CheckResponseWithError, len(results))
|
||||
for i, result := range results {
|
||||
responses[i] = &rts.CheckResponseWithError{ //nolint:gosec // Snaptoken is not a credential, just a placeholder value.
|
||||
Allowed: result.Membership == IsMember,
|
||||
Error: batchResultErrMsg(result, strict),
|
||||
Snaptoken: "not yet implemented",
|
||||
}
|
||||
}
|
||||
|
||||
return connect.NewResponse(&rts.BatchCheckResponse{
|
||||
Results: responses,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// batchResultErrMsg returns an error string for a single batch check result.
|
||||
// In strict mode a limitation is surfaced so callers can distinguish
|
||||
// "definitely not a member" from "could not determine membership".
|
||||
func batchResultErrMsg(r Result, strict bool) string {
|
||||
if r.Err != nil {
|
||||
return r.Err.Error()
|
||||
}
|
||||
if r.Membership == MembershipUnknown && r.Limitation != "" && strict {
|
||||
return string(r.Limitation)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user