Files
upstream-ory-keto/internal/check/handler.go
upstream-mirror 9456a4e922
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
snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:08:34 +05:00

527 lines
14 KiB
Go

// 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 ""
}