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
527 lines
14 KiB
Go
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 ""
|
|
}
|