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

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

34
ketoapi/cmd_output.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package ketoapi
func (r *RelationTuple) Header() []string {
return []string{
"NAMESPACE",
"OBJECT ID",
"RELATION NAME",
"SUBJECT",
}
}
func (r *RelationTuple) Columns() []string {
sub := "<!error no subject>"
switch {
case r.SubjectID != nil:
sub = *r.SubjectID
case r.SubjectSet != nil:
sub = r.SubjectSet.String()
}
return []string{
r.Namespace,
r.Object,
r.Relation,
sub,
}
}
func (r *RelationTuple) Interface() interface{} {
return r
}

189
ketoapi/enc_proto.go Normal file
View File

@@ -0,0 +1,189 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package ketoapi
import (
"github.com/pkg/errors"
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
)
type (
TupleData interface {
GetSubject() *rts.Subject
GetObject() string
GetNamespace() string
GetRelation() string
}
queryData interface {
GetSubject() *rts.Subject
GetObject() *string
GetNamespace() *string
GetRelation() *string
}
)
func (r *RelationTuple) FromDataProvider(d TupleData) (*RelationTuple, error) {
switch s := d.GetSubject().GetRef().(type) {
case nil:
return nil, errors.WithStack(ErrNilSubject())
case *rts.Subject_Set:
r.SubjectSet = &SubjectSet{
Namespace: s.Set.Namespace,
Object: s.Set.Object,
Relation: s.Set.Relation,
}
case *rts.Subject_Id:
r.SubjectID = new(s.Id)
}
r.Object = d.GetObject()
r.Namespace = d.GetNamespace()
r.Relation = d.GetRelation()
return r, nil
}
func (r *RelationTuple) ToProto() *rts.RelationTuple {
res := &rts.RelationTuple{
Namespace: r.Namespace,
Object: r.Object,
Relation: r.Relation,
}
if r.SubjectID != nil {
res.Subject = rts.NewSubjectID(*r.SubjectID)
} else {
res.Subject = rts.NewSubjectSet(r.SubjectSet.Namespace, r.SubjectSet.Object, r.SubjectSet.Relation)
}
return res
}
func (r *RelationTuple) FromProto(proto *rts.RelationTuple) *RelationTuple {
r = &RelationTuple{
Namespace: proto.Namespace,
Object: proto.Object,
Relation: proto.Relation,
}
switch subject := proto.Subject.Ref.(type) {
case *rts.Subject_Id:
r.SubjectID = new(subject.Id)
case *rts.Subject_Set:
r.SubjectSet = &SubjectSet{
Namespace: subject.Set.Namespace,
Object: subject.Set.Object,
Relation: subject.Set.Relation,
}
}
return r
}
func (q *RelationQuery) FromDataProvider(d queryData) *RelationQuery {
q.Namespace = d.GetNamespace()
q.Object = d.GetObject()
q.Relation = d.GetRelation()
q.SubjectID = nil
q.SubjectSet = nil
if s := d.GetSubject(); s != nil {
switch sub := s.Ref.(type) {
case *rts.Subject_Id:
q.SubjectID = new(sub.Id)
case *rts.Subject_Set:
q.SubjectSet = &SubjectSet{
Namespace: sub.Set.Namespace,
Object: sub.Set.Object,
Relation: sub.Set.Relation,
}
}
}
return q
}
func (q *RelationQuery) ToProto() *rts.RelationQuery {
res := &rts.RelationQuery{
Namespace: q.Namespace,
Object: q.Object,
Relation: q.Relation,
}
if q.SubjectID != nil {
res.Subject = rts.NewSubjectID(*q.SubjectID)
} else if q.SubjectSet != nil {
res.Subject = rts.NewSubjectSet(q.SubjectSet.Namespace, q.SubjectSet.Object, q.SubjectSet.Relation)
}
return res
}
func (t *Tree[NodeT]) ToProto() *rts.SubjectTree {
res := &rts.SubjectTree{
NodeType: t.Type.ToProto(),
Children: make([]*rts.SubjectTree, len(t.Children)),
}
res.Tuple = t.Tuple.ToProto()
//lint:ignore SA1019 backwards compatibility
//nolint:staticcheck
res.Subject = res.Tuple.Subject
for i := range t.Children {
res.Children[i] = t.Children[i].ToProto()
}
return res
}
func TreeFromProto[T tuple[T]](pt *rts.SubjectTree) *Tree[T] {
t := new(Tree[T])
t.Type = TreeNodeType("").FromProto(pt.NodeType)
var tuple T
if pt.Tuple == nil {
// legacy case: fetch from deprecated fields
//lint:ignore SA1019 backwards compatibility
//nolint:staticcheck
switch sub := pt.Subject.Ref.(type) {
case *rts.Subject_Id:
pt.Tuple.Subject = rts.NewSubjectID(sub.Id)
case *rts.Subject_Set:
pt.Tuple.Subject = rts.NewSubjectSet(
sub.Set.Namespace,
sub.Set.Object,
sub.Set.Relation,
)
}
}
t.Tuple = tuple.FromProto(pt.Tuple)
t.Children = make([]*Tree[T], len(pt.Children))
for i := range pt.Children {
t.Children[i] = TreeFromProto[T](pt.Children[i])
}
return t
}
func (t TreeNodeType) ToProto() rts.NodeType {
switch t {
case TreeNodeLeaf:
return rts.NodeType_NODE_TYPE_LEAF
case TreeNodeUnion:
return rts.NodeType_NODE_TYPE_UNION
case TreeNodeExclusion:
return rts.NodeType_NODE_TYPE_EXCLUSION
case TreeNodeIntersection:
return rts.NodeType_NODE_TYPE_INTERSECTION
}
return rts.NodeType_NODE_TYPE_UNSPECIFIED
}
func (TreeNodeType) FromProto(pt rts.NodeType) TreeNodeType {
switch pt {
case rts.NodeType_NODE_TYPE_LEAF:
return TreeNodeLeaf
case rts.NodeType_NODE_TYPE_UNION:
return TreeNodeUnion
case rts.NodeType_NODE_TYPE_EXCLUSION:
return TreeNodeExclusion
case rts.NodeType_NODE_TYPE_INTERSECTION:
return TreeNodeIntersection
}
return TreeNodeUnspecified
}

153
ketoapi/enc_string.go Normal file
View File

@@ -0,0 +1,153 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package ketoapi
import (
"fmt"
"strings"
"github.com/ory/herodot"
"github.com/pkg/errors"
)
func ErrMalformedInput() *herodot.DefaultError {
return herodot.ErrBadRequest().WithError("malformed string input")
}
func (r *RelationTuple) String() string {
if r == nil {
return ""
}
sb := strings.Builder{}
sb.WriteString(r.Namespace)
sb.WriteRune(':')
sb.WriteString(r.Object)
sb.WriteRune('#')
sb.WriteString(r.Relation)
sb.WriteRune('@')
if r.SubjectID != nil {
sb.WriteString(*r.SubjectID)
} else if r.SubjectSet != nil {
sb.WriteString(r.SubjectSet.String())
} else {
sb.WriteString("<ERROR: no subject>")
}
return sb.String()
}
func (r *RelationTuple) FromString(s string) (*RelationTuple, error) {
var (
objectAndRelationAndSubject string
relationAndSubject string
subject string
ok bool
)
if r.Namespace, objectAndRelationAndSubject, ok = strings.Cut(s, ":"); !ok {
return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected input to contain ':'"))
}
if r.Object, relationAndSubject, ok = strings.Cut(objectAndRelationAndSubject, "#"); !ok {
return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected input to contain '#'"))
}
if r.Relation, subject, ok = strings.Cut(relationAndSubject, "@"); !ok {
return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected input to contain '@'"))
}
// remove optional brackets around the subject set
subject = strings.Trim(subject, "()")
if strings.Contains(subject, ":") {
subSet, err := (&SubjectSet{}).FromString(subject)
if err != nil {
return nil, err
}
r.SubjectSet = subSet
} else {
r.SubjectID = &subject
}
return r, nil
}
func (s *SubjectSet) String() string {
if s.Relation == "" {
return fmt.Sprintf("%s:%s", s.Namespace, s.Object)
}
return fmt.Sprintf("%s:%s#%s", s.Namespace, s.Object, s.Relation)
}
func (s *SubjectSet) FromString(str string) (*SubjectSet, error) {
// If there is no '#' we have a subject set without a relation, such as
// Users:Bob, which just means that the relation is empty.
namespaceAndObject, relation, _ := strings.Cut(str, "#")
namespace, object, ok := strings.Cut(namespaceAndObject, ":")
if !ok {
return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected subject set to contain ':'"))
}
return &SubjectSet{
Namespace: namespace,
Object: object,
Relation: relation,
}, nil
}
func (t TreeNodeType) String() string {
return string(t)
}
func (t *Tree[NodeT]) Label() string {
if t == nil {
return ""
}
return t.Tuple.String()
}
func (t *Tree[NodeT]) String() string {
if t == nil {
return ""
}
nodeLabel := t.Label()
if t.Type == TreeNodeLeaf {
return fmt.Sprintf("∋ %s️", nodeLabel)
}
children := make([]string, len(t.Children))
for i, c := range t.Children {
var indent string
if i == len(t.Children)-1 {
indent = " "
} else {
indent = "│ "
}
children[i] = strings.Join(strings.Split(c.String(), "\n"), "\n"+indent)
}
setOperation := ""
switch t.Type {
case TreeNodeIntersection:
setOperation = "and"
case TreeNodeUnion:
setOperation = "or"
case TreeNodeExclusion:
setOperation = `\`
case TreeNodeNot:
setOperation = `not`
case TreeNodeTupleToSubjectSet:
setOperation = "┐ tuple to userset"
case TreeNodeComputedSubjectSet:
setOperation = "┐ computed userset"
}
boxSymbol := "├"
if len(children) == 1 {
boxSymbol = "└"
}
return fmt.Sprintf("%s %s\n%s──%s", setOperation, nodeLabel, boxSymbol, strings.Join(children, "\n└──"))
}

413
ketoapi/enc_test.go Normal file
View File

@@ -0,0 +1,413 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package ketoapi
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
)
func TestRelationTuple(t *testing.T) {
t.Run("method=string encoding", func(t *testing.T) {
for _, tc := range []struct {
tuple *RelationTuple
expected string
}{
{ // full tuple
tuple: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: new("s"),
},
expected: "n:o#r@s",
},
{ // skip '#' in subject set when relation is empty
tuple: &RelationTuple{
Namespace: "groups",
Object: "dev",
Relation: "member",
SubjectSet: &SubjectSet{
Namespace: "users",
Object: "user",
},
},
expected: "groups:dev#member@users:user",
},
} {
t.Run("case="+tc.expected, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.tuple.String())
})
}
})
t.Run("method=string decoding", func(t *testing.T) {
for _, tc := range []struct {
enc string
err error
expected *RelationTuple
}{
{
enc: "n:o#r@s",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: new("s"),
},
},
{
enc: "n:o#r@n:o#r",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
Relation: "r",
},
},
},
{
enc: "n:o#r@n:o#...",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
Relation: "...",
},
},
},
{
enc: "n:o#r@n:o#",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
},
},
},
{
enc: "n:o#r@n:o",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
},
},
},
{
enc: "n:o#r@(n:o#r)",
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
Relation: "r",
},
},
},
{
enc: "#dev:@ory#:working:@projects:keto#awesome",
expected: &RelationTuple{
Namespace: "#dev",
Object: "@ory",
Relation: ":working:",
SubjectSet: &SubjectSet{
Namespace: "projects",
Object: "keto",
Relation: "awesome",
},
},
},
{
enc: "no-colon#in@this",
err: ErrMalformedInput(),
},
{
enc: "no:hash-in@this",
err: ErrMalformedInput(),
},
{
enc: "no:at#in-this",
err: ErrMalformedInput(),
},
} {
t.Run(fmt.Sprintf("string=%s", tc.enc), func(t *testing.T) {
actual, err := (&RelationTuple{}).FromString(tc.enc)
assert.True(t, errors.Is(err, tc.err), "%+v", err)
assert.Equal(t, tc.expected, actual)
})
}
})
t.Run("case=url encoding-decoding", func(t *testing.T) {
for i, r := range []*RelationTuple{
{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: new("s"),
},
{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "sn",
Object: "so",
Relation: "sr",
},
},
{
SubjectID: new(""),
},
} {
t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
vals := r.ToURLQuery()
res, err := (&RelationTuple{}).FromURLQuery(vals)
require.NoError(t, err, "raw: %+v, enc: %+v", r, vals)
assert.Equal(t, r, res)
})
}
})
t.Run("case=url decoding-encoding", func(t *testing.T) {
for i, v := range []url.Values{
{
"namespace": []string{"n"},
"object": []string{"o"},
"relation": []string{"r"},
"subject_id": []string{"foo"},
},
{
"namespace": []string{"n"},
"object": []string{"o"},
"relation": []string{"r"},
"subject_set.namespace": []string{"sn"},
"subject_set.object": []string{"so"},
"subject_set.relation": []string{"sr"},
},
} {
t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
rt, err := (&RelationTuple{}).FromURLQuery(v)
require.NoError(t, err)
q := rt.ToURLQuery()
assert.Equal(t, v, q)
})
}
})
t.Run("case=proto decoding", func(t *testing.T) {
for i, tc := range []struct {
proto TupleData
expected *RelationTuple
err error
}{
{
proto: &rts.RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
Subject: nil,
},
err: ErrNilSubject(),
},
{
proto: &rts.RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
Subject: &rts.Subject{
Ref: &rts.Subject_Set{
Set: &rts.SubjectSet{
Namespace: "n",
Object: "o",
Relation: "r",
},
},
},
},
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "n",
Object: "o",
Relation: "r",
},
},
},
{
proto: &rts.RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
Subject: &rts.Subject{
Ref: &rts.Subject_Id{
Id: "user",
},
},
},
expected: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: new("user"),
},
},
} {
t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
actual, err := (&RelationTuple{}).FromDataProvider(tc.proto)
require.ErrorIs(t, err, tc.err)
assert.Equal(t, tc.expected, actual)
})
}
})
t.Run("format=JSON", func(t *testing.T) {
t.Run("direction=encoding-decoding", func(t *testing.T) {
for _, tc := range []struct {
name string
rt *RelationTuple
expected string
}{
{
name: "with subject ID",
rt: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectID: new("s"),
},
expected: `
{
"namespace": "n",
"object": "o",
"relation": "r",
"subject_id": "s"
}`,
},
{
name: "with subject set",
rt: &RelationTuple{
Namespace: "n",
Object: "o",
Relation: "r",
SubjectSet: &SubjectSet{
Namespace: "sn",
Object: "so",
Relation: "sr",
},
},
expected: `
{
"namespace": "n",
"object": "o",
"relation": "r",
"subject_set": {
"namespace": "sn",
"object": "so",
"relation": "sr"
}
}`,
},
} {
t.Run("case="+tc.name, func(t *testing.T) {
raw, err := json.Marshal(tc.rt)
require.NoError(t, err)
assert.JSONEq(t, tc.expected, string(raw))
var dec RelationTuple
require.NoError(t, json.Unmarshal(raw, &dec))
assert.Equal(t, tc.rt, &dec)
})
}
})
})
}
func TestRelationQuery(t *testing.T) {
t.Run("case=url encoding-decoding-encoding", func(t *testing.T) {
for i, tc := range []struct {
v url.Values
r *RelationQuery
}{
{
v: url.Values{
"namespace": []string{"n"},
"object": []string{"o"},
"relation": []string{"r"},
"subject_id": []string{"foo"},
},
r: &RelationQuery{
Namespace: new("n"),
Object: new("o"),
Relation: new("r"),
SubjectID: new("foo"),
},
},
{
v: url.Values{
"namespace": []string{"n"},
"object": []string{"o"},
"relation": []string{"r"},
"subject_set.namespace": []string{"sn"},
"subject_set.object": []string{"so"},
"subject_set.relation": []string{"sr"},
},
r: &RelationQuery{
Namespace: new("n"),
Object: new("o"),
Relation: new("r"),
SubjectSet: &SubjectSet{
Namespace: "sn",
Object: "so",
Relation: "sr",
},
},
},
{
v: url.Values{
"namespace": []string{"n"},
"relation": []string{"r"},
},
r: &RelationQuery{
Namespace: new("n"),
Relation: new("r"),
},
},
} {
t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) {
enc := tc.r.ToURLQuery()
assert.Equal(t, tc.v, enc)
dec, err := (&RelationQuery{}).FromURLQuery(tc.v)
require.NoError(t, err)
assert.Equal(t, tc.r, dec)
})
}
})
}

127
ketoapi/enc_url_query.go Normal file
View File

@@ -0,0 +1,127 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package ketoapi
import (
"net/url"
"github.com/pkg/errors"
)
func (q *RelationQuery) FromURLQuery(query url.Values) (*RelationQuery, error) {
if q == nil {
q = &RelationQuery{}
}
if query.Has("subject") {
return nil, ErrDroppedSubjectKey()
}
// reset subject
q.SubjectID = nil
q.SubjectSet = nil
switch {
case !query.Has(SubjectIDKey) && !query.Has(SubjectSetNamespaceKey) && !query.Has(SubjectSetObjectKey) && !query.Has(SubjectSetRelationKey):
// was not queried for the subject
case query.Has(SubjectIDKey) && (query.Has(SubjectSetNamespaceKey) || query.Has(SubjectSetObjectKey) || query.Has(SubjectSetRelationKey)):
return nil, ErrDuplicateSubject().WithDebugf("please provide either %s or all of %s, %s, and %s", SubjectIDKey, SubjectSetNamespaceKey, SubjectSetObjectKey, SubjectSetRelationKey)
case query.Has(SubjectIDKey):
q.SubjectID = new(query.Get(SubjectIDKey))
case query.Has(SubjectSetNamespaceKey) && query.Has(SubjectSetObjectKey) && query.Has(SubjectSetRelationKey):
q.SubjectSet = &SubjectSet{
Namespace: query.Get(SubjectSetNamespaceKey),
Object: query.Get(SubjectSetObjectKey),
Relation: query.Get(SubjectSetRelationKey),
}
default:
return nil, ErrIncompleteSubject()
}
if query.Has(NamespaceKey) {
q.Namespace = new(query.Get(NamespaceKey))
}
if query.Has(ObjectKey) {
q.Object = new(query.Get(ObjectKey))
}
if query.Has(RelationKey) {
q.Relation = new(query.Get(RelationKey))
}
return q, nil
}
func (q *RelationQuery) ToURLQuery() url.Values {
v := make(url.Values, 7)
if q.Namespace != nil {
v.Add(NamespaceKey, *q.Namespace)
}
if q.Relation != nil {
v.Add(RelationKey, *q.Relation)
}
if q.Object != nil {
v.Add(ObjectKey, *q.Object)
}
if q.SubjectID != nil {
v.Add(SubjectIDKey, *q.SubjectID)
} else if q.SubjectSet != nil {
v.Add(SubjectSetNamespaceKey, q.SubjectSet.Namespace)
v.Add(SubjectSetObjectKey, q.SubjectSet.Object)
v.Add(SubjectSetRelationKey, q.SubjectSet.Relation)
}
return v
}
func (r *RelationTuple) FromURLQuery(query url.Values) (*RelationTuple, error) {
q, err := (&RelationQuery{}).FromURLQuery(query)
if err != nil {
return nil, err
}
if q.SubjectID == nil && q.SubjectSet == nil {
return nil, errors.WithStack(ErrNilSubject())
}
if q.Namespace == nil || q.Object == nil || q.Relation == nil {
return nil, errors.WithStack(ErrIncompleteTuple())
}
r.Namespace = *q.Namespace
r.Object = *q.Object
r.Relation = *q.Relation
r.SubjectID = q.SubjectID
r.SubjectSet = q.SubjectSet
return r, nil
}
func (r *RelationTuple) ToURLQuery() url.Values {
return (&RelationQuery{
Namespace: &r.Namespace,
Object: &r.Object,
Relation: &r.Relation,
SubjectID: r.SubjectID,
SubjectSet: r.SubjectSet,
}).ToURLQuery()
}
func (s *SubjectSet) FromURLQuery(values url.Values) *SubjectSet {
if s == nil {
s = &SubjectSet{}
}
s.Namespace = values.Get(NamespaceKey)
s.Relation = values.Get(RelationKey)
s.Object = values.Get(ObjectKey)
return s
}
func (s *SubjectSet) ToURLQuery() url.Values {
return url.Values{
NamespaceKey: []string{s.Namespace},
ObjectKey: []string{s.Object},
RelationKey: []string{s.Relation},
}
}

View File

@@ -0,0 +1,282 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package ketoapi
import (
"encoding/json"
"fmt"
"github.com/ory/herodot"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
)
var ErrUnknownNodeType = errors.New("unknown node type")
func ErrDroppedSubjectKey() *herodot.DefaultError {
return herodot.ErrBadRequest().WithDebug(`provide "subject_id" or "subject_set.*"; support for "subject" was dropped`)
}
func ErrDuplicateSubject() *herodot.DefaultError {
return herodot.ErrBadRequest().WithError("exactly one of subject_set or subject_id has to be provided")
}
func ErrIncompleteSubject() *herodot.DefaultError {
return herodot.ErrBadRequest().WithError(`incomplete subject, provide "subject_id" or a complete "subject_set.*"`)
}
func ErrNilSubject() *herodot.DefaultError {
return herodot.ErrBadRequest().WithError("subject is not allowed to be nil").WithDebug("Please provide a subject.")
}
func ErrIncompleteTuple() *herodot.DefaultError {
return herodot.ErrBadRequest().WithError(`incomplete tuple, provide "namespace", "object", "relation", and a subject`)
}
// swagger:model namespace
type Namespace struct {
// Name of the namespace.
Name string `json:"name"`
}
// Relationship
//
// swagger:model relationship
type RelationTuple struct {
// Namespace of the Relation Tuple
//
// required: true
Namespace string `json:"namespace"`
// Object of the Relation Tuple
//
// required: true
Object string `json:"object"`
// Relation of the Relation Tuple
//
// required: true
Relation string `json:"relation"`
// SubjectID of the Relation Tuple
//
// Either SubjectSet or SubjectID can be provided.
SubjectID *string `json:"subject_id,omitempty"`
// SubjectSet of the Relation Tuple
//
// Either SubjectSet or SubjectID can be provided.
//
// swagger:allOf
SubjectSet *SubjectSet `json:"subject_set,omitempty"`
}
// swagger:model subjectSet
type SubjectSet struct {
// Namespace of the Subject Set
//
// required: true
Namespace string `json:"namespace"`
// Object of the Subject Set
//
// required: true
Object string `json:"object"`
// Relation of the Subject Set
//
// required: true
Relation string `json:"relation"`
}
// Relation Query
//
// swagger:model relationQuery
type RelationQuery struct {
// Namespace to query
Namespace *string `json:"namespace"`
// Object to query
Object *string `json:"object"`
// Relation to query
Relation *string `json:"relation"`
// SubjectID to query
//
// Either SubjectSet or SubjectID can be provided.
SubjectID *string `json:"subject_id,omitempty"`
// SubjectSet to query
//
// Either SubjectSet or SubjectID can be provided.
//
// swagger:allOf
SubjectSet *SubjectSet `json:"subject_set,omitempty"`
}
// Payload for patching a relationship
//
// swagger:model relationshipPatch
type PatchDelta struct {
Action PatchAction `json:"action"`
RelationTuple *RelationTuple `json:"relation_tuple"`
}
// swagger:enum PatchAction
type PatchAction string
const (
ActionInsert PatchAction = "insert"
ActionDelete PatchAction = "delete"
)
const (
NamespaceKey = "namespace"
ObjectKey = "object"
RelationKey = "relation"
SubjectIDKey = "subject_id"
SubjectSetNamespaceKey = "subject_set.namespace"
SubjectSetObjectKey = "subject_set.object"
SubjectSetRelationKey = "subject_set.relation"
)
var RelationQueryKeys = []string{
NamespaceKey,
ObjectKey,
RelationKey,
SubjectIDKey,
SubjectSetNamespaceKey,
SubjectSetObjectKey,
SubjectSetRelationKey,
"subject", // We have a more specific error message for this key.
}
// Paginated Relationship List
//
// swagger:model relationships
type GetResponse struct {
RelationTuples []*RelationTuple `json:"relation_tuples"`
// The opaque token to provide in a subsequent request
// to get the next page. It is the empty string iff this is
// the last page.
NextPageToken string `json:"next_page_token"`
}
// Relationship Namespace List
//
// swagger:model relationshipNamespaces
type GetNamespacesResponse struct {
Namespaces []Namespace `json:"namespaces"`
}
func (r *RelationTuple) ToLoggerFields() logrus.Fields {
fields := make(logrus.Fields, 7)
q := r.ToURLQuery()
for k := range q {
fields[k] = q.Get(k)
}
return fields
}
func (r *RelationTuple) Validate() error {
if r.SubjectSet == nil && r.SubjectID == nil {
return errors.WithStack(ErrNilSubject())
}
return nil
}
// swagger:enum ExpandNodeType
type ExpandNodeType TreeNodeType
// swagger:enum TreeNodeType
type TreeNodeType string
const (
TreeNodeUnion TreeNodeType = "union"
TreeNodeExclusion TreeNodeType = "exclusion"
TreeNodeIntersection TreeNodeType = "intersection"
TreeNodeLeaf TreeNodeType = "leaf"
TreeNodeTupleToSubjectSet TreeNodeType = "tuple_to_subject_set"
TreeNodeComputedSubjectSet TreeNodeType = "computed_subject_set"
TreeNodeNot TreeNodeType = "not"
TreeNodeUnspecified TreeNodeType = "unspecified"
)
func (t *TreeNodeType) UnmarshalJSON(v []byte) error {
var s string
if err := json.Unmarshal(v, &s); err != nil {
return err
}
switch nt := TreeNodeType(s); nt {
case TreeNodeUnion, TreeNodeExclusion, TreeNodeIntersection, TreeNodeLeaf, TreeNodeTupleToSubjectSet, TreeNodeComputedSubjectSet, TreeNodeNot, TreeNodeUnspecified:
*t = nt
default:
return ErrUnknownNodeType
}
return nil
}
type tuple[T any] interface {
fmt.Stringer
ToProto() *rts.RelationTuple
FromProto(*rts.RelationTuple) T
}
// Tree is a generic tree of either internal relationships (with UUIDs for
// objects, etc.) or API relationships (with strings for objects, etc.).
type Tree[T tuple[T]] struct {
// Propagate all struct changes to `swaggerOnlyExpandTree` as well.
// The type of the node.
//
// required: true
Type TreeNodeType `json:"type"`
// The children of the node, possibly none.
Children []*Tree[T] `json:"children,omitempty"`
// The relation tuple the node represents.
Tuple T `json:"tuple"`
}
// IMPORTANT: We need a manual instantiation of the generic Tree[T] for the
// OpenAPI spec, since go-swagger does not understand generics :(.
// This can be fixed by using grpc-gateway.
// swagger:model expandedPermissionTree
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type swaggerOnlyExpandTree struct {
// The type of the node.
//
// required: true
Type TreeNodeType `json:"type"`
// The children of the node, possibly none.
Children []*swaggerOnlyExpandTree `json:"children,omitempty"`
// The relation tuple the node represents.
Tuple *RelationTuple `json:"tuple"`
}
type ParseError struct {
Message string `json:"message"`
Start SourcePosition `json:"start"`
End SourcePosition `json:"end"`
}
type SourcePosition struct {
Line int `json:"Line"`
Col int `json:"column"`
}
// CheckOPLSyntaxResponse represents the response for an OPL syntax check request.
//
// swagger:model checkOplSyntaxResult
type CheckOPLSyntaxResponse struct {
// The list of syntax errors
//
// required: false
Errors []*ParseError `json:"errors,omitempty"`
}