-
Notifications
You must be signed in to change notification settings - Fork 0
/
tuples.go
318 lines (258 loc) · 9.46 KB
/
tuples.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
package fgax
import (
"context"
"regexp"
"strings"
openfga "github.com/openfga/go-sdk"
ofgaclient "github.com/openfga/go-sdk/client"
)
// setup relations for use in creating tuples
const (
// SystemAdminRole is the role for system admins that have the highest level of access
SystemAdminRole = "system_admin"
// MemberRelation is the relation for members of an entity
MemberRelation = "member"
// AdminRelation is the relation for admins of an entity
AdminRelation = "admin"
// OwnerRelation is the relation for owners of an entity
OwnerRelation = "owner"
// ParentRelation is the relation for parents of an entity
ParentRelation = "parent"
// AssigneeRoleRelation is the relation for assignees of an entity
RoleRelation = "assignee"
// CanView is the relation for viewing an entity
CanView = "can_view"
// CanEdit is the relation for editing an entity
CanEdit = "can_edit"
// CanDelete is the relation for deleting an entity
CanDelete = "can_delete"
)
const (
// defaultPageSize is based on the openfga max of 100
defaultPageSize = 100
// maxWrites is the maximum number of Writes and Deletes supported by the OpenFGA transactional write api
// see https://openfga.dev/docs/interacting/transactional-writes for more details
maxWrites = 10
)
// TupleKey represents a relationship tuple in OpenFGA
type TupleKey struct {
// Subject is the entity that is the subject of the relationship, usually a user
Subject Entity
// Object is the entity that is the object of the relationship, (e.g. organization, project, document, etc)
Object Entity
// Relation is the relationship between the subject and object
Relation Relation `json:"relation"`
}
// TupleRequest is the fields needed to check a tuple in the FGA store
type TupleRequest struct {
// ObjectID is the identifier of the object that the subject is related to
ObjectID string
// ObjectType is the type of object that the subject is related to
ObjectType string
// SubjectID is the identifier of the subject that is related to the object
SubjectID string
// SubjectType is the type of subject that is related to the object
SubjectType string
// Relation is the relationship between the subject and object
Relation string
}
func NewTupleKey() TupleKey { return TupleKey{} }
// entityRegex is used to validate that a string represents an Entity/EntitySet
// and helps to convert from a string representation into an Entity struct.
var entityRegex = regexp.MustCompile(`([A-za-z0-9_][A-za-z0-9_-]*):([A-za-z0-9_][A-za-z0-9_@.+-]*)(#([A-za-z0-9_][A-za-z0-9_-]*))?`)
// Kind represents the type of the entity in OpenFGA.
type Kind string
// String implements the Stringer interface.
func (k Kind) String() string {
return strings.ToLower(string(k))
}
// Relation represents the type of relation between entities in OpenFGA.
type Relation string
// String implements the Stringer interface.
func (r Relation) String() string {
return strings.ToLower(string(r))
}
// Entity represents an entity/entity-set in OpenFGA.
// Example: `user:<user-id>`, `org:<org-id>#member`
type Entity struct {
Kind Kind
Identifier string
Relation Relation
}
// String returns a string representation of the entity/entity-set.
func (e *Entity) String() string {
if e.Relation == "" {
return e.Kind.String() + ":" + e.Identifier
}
return e.Kind.String() + ":" + e.Identifier + "#" + e.Relation.String()
}
// ParseEntity will parse a string representation into an Entity. It expects to
// find entities of the form:
// - <entityType>:<Identifier>
// eg. organization:datum
// - <entityType>:<Identifier>#<relationship-set>
// eg. organization:datum#member
func ParseEntity(s string) (Entity, error) {
// entities should only contain a single colon
c := strings.Count(s, ":")
if c != 1 {
return Entity{}, newInvalidEntityError(s)
}
match := entityRegex.FindStringSubmatch(s)
if match == nil {
return Entity{}, newInvalidEntityError(s)
}
// Extract and return the relevant information from the sub-matches.
return Entity{
Kind: Kind(match[1]),
Identifier: match[2],
Relation: Relation(match[4]),
}, nil
}
// tupleKeyToWriteRequest converts a TupleKey to a ClientTupleKey to send to FGA
func tupleKeyToWriteRequest(writes []TupleKey) (w []ofgaclient.ClientTupleKey) {
for _, k := range writes {
ctk := ofgaclient.ClientTupleKey{}
ctk.SetObject(k.Object.String())
ctk.SetUser(k.Subject.String())
ctk.SetRelation(k.Relation.String())
w = append(w, ctk)
}
return
}
// tupleKeyToDeleteRequest converts a TupleKey to a TupleKeyWithoutCondition to send to FGA
func tupleKeyToDeleteRequest(deletes []TupleKey) (d []openfga.TupleKeyWithoutCondition) {
for _, k := range deletes {
ctk := openfga.TupleKeyWithoutCondition{}
ctk.SetObject(k.Object.String())
ctk.SetUser(k.Subject.String())
ctk.SetRelation(k.Relation.String())
d = append(d, ctk)
}
return
}
// WriteTupleKeys takes a tuples keys, converts them to a client write request, which can contain up to 10 writes and deletes,
// and executes in a single transaction
func (c *Client) WriteTupleKeys(ctx context.Context, writes []TupleKey, deletes []TupleKey) (*ofgaclient.ClientWriteResponse, error) {
opts := ofgaclient.ClientWriteOptions{AuthorizationModelId: openfga.PtrString(c.Config.AuthorizationModelId)}
body := ofgaclient.ClientWriteRequest{
Writes: tupleKeyToWriteRequest(writes),
Deletes: tupleKeyToDeleteRequest(deletes),
}
resp, err := c.Ofga.Write(ctx).Body(body).Options(opts).Execute()
if err != nil {
c.Logger.Infow("error writing relationship tuples", "error", err.Error(), "user", resp.Writes)
return resp, err
}
for _, writes := range resp.Writes {
if writes.Error != nil {
c.Logger.Errorw("error creating relationship tuples", "user", writes.TupleKey.User, "relation", writes.TupleKey.Relation, "object", writes.TupleKey.Object)
return resp, newWritingTuplesError(writes.TupleKey.User, writes.TupleKey.Relation, writes.TupleKey.Object, "writing", err)
}
}
for _, deletes := range resp.Deletes {
if deletes.Error != nil {
c.Logger.Errorw("error deleting relationship tuples", "user", deletes.TupleKey.User, "relation", deletes.TupleKey.Relation, "object", deletes.TupleKey.Object)
return resp, newWritingTuplesError(deletes.TupleKey.User, deletes.TupleKey.Relation, deletes.TupleKey.Object, "writing", err)
}
}
return resp, nil
}
// deleteRelationshipTuple deletes a relationship tuple in the openFGA store
func (c *Client) deleteRelationshipTuple(ctx context.Context, tuples []openfga.TupleKeyWithoutCondition) (*ofgaclient.ClientWriteResponse, error) {
if len(tuples) == 0 {
return nil, nil
}
opts := ofgaclient.ClientWriteOptions{AuthorizationModelId: openfga.PtrString(c.Config.AuthorizationModelId)}
resp, err := c.Ofga.DeleteTuples(ctx).Body(tuples).Options(opts).Execute()
if err != nil {
c.Logger.Errorw("error deleting relationship tuples", "error", err.Error())
return resp, err
}
for _, del := range resp.Deletes {
if del.Error != nil {
c.Logger.Errorw("error deleting relationship tuples", "user", del.TupleKey.User, "relation", del.TupleKey.Relation, "object", del.TupleKey.Object)
return resp, newWritingTuplesError(del.TupleKey.User, del.TupleKey.Relation, del.TupleKey.Object, "deleting", err)
}
}
return resp, nil
}
// getAllTuples gets all the relationship tuples in the openFGA store
func (c *Client) getAllTuples(ctx context.Context) ([]openfga.Tuple, error) {
var tuples []openfga.Tuple
opts := ofgaclient.ClientReadOptions{
PageSize: openfga.PtrInt32(defaultPageSize),
ContinuationToken: openfga.PtrString(" "),
}
notComplete := true
// paginate through all the tuples
for notComplete {
resp, err := c.Ofga.Read(ctx).Options(opts).Execute()
if err != nil {
c.Logger.Errorw("error getting relationship tuples", "error", err.Error())
return nil, err
}
tuples = append(tuples, resp.GetTuples()...)
if resp.ContinuationToken == "" {
notComplete = false
} else {
opts.ContinuationToken = &resp.ContinuationToken
}
}
return tuples, nil
}
// DeleteAllObjectRelations deletes all the relationship tuples for a given object
func (c *Client) DeleteAllObjectRelations(ctx context.Context, object string) error {
// validate object is not empty
if object == "" {
return ErrMissingObjectOnDeletion
}
match := entityRegex.FindStringSubmatch(object)
if match == nil {
return newInvalidEntityError(object)
}
tuples, err := c.getAllTuples(ctx)
if err != nil {
return err
}
var tuplesToDelete []openfga.TupleKeyWithoutCondition
// check all the tuples for the object
for _, t := range tuples {
if t.Key.Object == object {
k := openfga.TupleKeyWithoutCondition{
User: t.Key.User,
Relation: t.Key.Relation,
Object: t.Key.Object,
}
tuplesToDelete = append(tuplesToDelete, k)
}
}
// delete the tuples in batches of 10, the max supported by the OpenFGA transactional write api
for i := 0; i < len(tuplesToDelete); i += maxWrites {
end := i + maxWrites
if end > len(tuplesToDelete) {
end = len(tuplesToDelete)
}
allTuples := tuplesToDelete[i:end]
if _, err := c.deleteRelationshipTuple(ctx, allTuples); err != nil {
return err
}
}
return nil
}
// GetTupleKey creates a Tuple key with the provided subject, object, and role
func GetTupleKey(req TupleRequest) TupleKey {
sub := Entity{
Kind: Kind(req.SubjectType),
Identifier: req.SubjectID,
}
object := Entity{
Kind: Kind(req.ObjectType),
Identifier: req.ObjectID,
}
return TupleKey{
Subject: sub,
Object: object,
Relation: Relation(req.Relation),
}
}