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:
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export KETO_WRITE_REMOTE="127.0.0.1:4467"
|
||||
|
||||
printf '%s\n' \
|
||||
'File:data.txt#viewer@user_alice' \
|
||||
'File:data.txt#viewer@user_bob' \
|
||||
'File:data.txt#viewer@apikey_ci-bot' | \
|
||||
keto relation-tuple parse -f - --format json | \
|
||||
keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \
|
||||
&& echo "Successfully created tuples" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
curl -X PATCH --fail --silent \
|
||||
-H 'Content-Type: application/json' \
|
||||
--retry 7 --retry-connrefused \
|
||||
--data '[
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_alice"}},
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_bob"}},
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"apikey_ci-bot"}}
|
||||
]' \
|
||||
http://127.0.0.1:4467/admin/relation-tuples > /dev/null \
|
||||
&& echo "Successfully created tuples" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1 @@
|
||||
Successfully created tuples
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js"
|
||||
import { relationTuples, write, writeService } from "@ory/keto-grpc-client"
|
||||
|
||||
const writeClient = new writeService.WriteServiceClient(
|
||||
"127.0.0.1:4467",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
|
||||
function insertSubjectID(subjectID) {
|
||||
const rt = new relationTuples.RelationTuple()
|
||||
rt.setNamespace("File")
|
||||
rt.setObject("data.txt")
|
||||
rt.setRelation("viewer")
|
||||
const sub = new relationTuples.Subject()
|
||||
sub.setId(subjectID)
|
||||
rt.setSubject(sub)
|
||||
const delta = new write.RelationTupleDelta()
|
||||
delta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT)
|
||||
delta.setRelationTuple(rt)
|
||||
return delta
|
||||
}
|
||||
|
||||
const req = new write.TransactRelationTuplesRequest()
|
||||
for (const id of ["user_alice", "user_bob", "apikey_ci-bot"]) {
|
||||
req.addRelationTupleDeltas(insertSubjectID(id))
|
||||
}
|
||||
|
||||
writeClient.transactRelationTuples(req, (error) => {
|
||||
if (error) {
|
||||
console.log("Encountered error", error)
|
||||
} else {
|
||||
console.log("Successfully created tuples")
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := rts.NewWriteServiceClient(conn)
|
||||
|
||||
insert := func(subjectID string) *rts.RelationTupleDelta {
|
||||
return &rts.RelationTupleDelta{
|
||||
Action: rts.RelationTupleDelta_ACTION_INSERT,
|
||||
RelationTuple: &rts.RelationTuple{
|
||||
Namespace: "File",
|
||||
Object: "data.txt",
|
||||
Relation: "viewer",
|
||||
Subject: rts.NewSubjectID(subjectID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*rts.RelationTupleDelta{
|
||||
insert("user_alice"),
|
||||
insert("user_bob"),
|
||||
insert("apikey_ci-bot"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Successfully created tuples")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export KETO_READ_REMOTE="127.0.0.1:4466"
|
||||
|
||||
keto check user_alice viewer File:data.txt --insecure-disable-transport-security
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
curl -G --silent \
|
||||
--retry 7 --retry-connrefused \
|
||||
--data-urlencode "namespace=File" \
|
||||
--data-urlencode "object=data.txt" \
|
||||
--data-urlencode "relation=viewer" \
|
||||
--data-urlencode "subject_id=user_alice" \
|
||||
http://127.0.0.1:4466/relation-tuples/check \
|
||||
| jq -r 'if .allowed == true then "Allowed" else "Denied" end'
|
||||
@@ -0,0 +1 @@
|
||||
Allowed
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js"
|
||||
import { relationTuples, check, checkService } from "@ory/keto-grpc-client"
|
||||
|
||||
const checkClient = new checkService.CheckServiceClient(
|
||||
"127.0.0.1:4466",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
|
||||
const req = new check.CheckRequest()
|
||||
req.setNamespace("File")
|
||||
req.setObject("data.txt")
|
||||
req.setRelation("viewer")
|
||||
const sub = new relationTuples.Subject()
|
||||
sub.setId("user_alice")
|
||||
req.setSubject(sub)
|
||||
|
||||
checkClient.check(req, (error, resp) => {
|
||||
if (error) {
|
||||
console.log("Encountered error:", error)
|
||||
} else {
|
||||
console.log(resp.getAllowed() ? "Allowed" : "Denied")
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := rts.NewCheckServiceClient(conn)
|
||||
|
||||
res, err := client.Check(context.Background(), &rts.CheckRequest{
|
||||
Namespace: "File",
|
||||
Object: "data.txt",
|
||||
Relation: "viewer",
|
||||
Subject: rts.NewSubjectID("user_alice"),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if res.Allowed {
|
||||
fmt.Println("Allowed")
|
||||
return
|
||||
}
|
||||
fmt.Println("Denied")
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export KETO_WRITE_REMOTE="127.0.0.1:4467"
|
||||
|
||||
# During the migration phase, write every new tuple as both SubjectID and SubjectSet.
|
||||
printf '%s\n' \
|
||||
'File:data.txt#viewer@user_charlie' \
|
||||
'File:data.txt#viewer@User:charlie' | \
|
||||
keto relation-tuple parse -f - --format json | \
|
||||
keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \
|
||||
&& echo "Successfully created tuples" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# During migration, write every new tuple as both SubjectID and SubjectSet.
|
||||
curl -X PATCH --fail --silent \
|
||||
-H 'Content-Type: application/json' \
|
||||
--retry 7 --retry-connrefused \
|
||||
--data '[
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_charlie"}},
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"User","object":"charlie","relation":""}}}
|
||||
]' \
|
||||
http://127.0.0.1:4467/admin/relation-tuples > /dev/null \
|
||||
&& echo "Successfully created tuples" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1 @@
|
||||
Successfully created tuples
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js"
|
||||
import { relationTuples, write, writeService } from "@ory/keto-grpc-client"
|
||||
|
||||
const writeClient = new writeService.WriteServiceClient(
|
||||
"127.0.0.1:4467",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
|
||||
// During migration, write every new tuple as both SubjectID and SubjectSet.
|
||||
function makeDelta(subject) {
|
||||
const rt = new relationTuples.RelationTuple()
|
||||
rt.setNamespace("File")
|
||||
rt.setObject("data.txt")
|
||||
rt.setRelation("viewer")
|
||||
rt.setSubject(subject)
|
||||
const delta = new write.RelationTupleDelta()
|
||||
delta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT)
|
||||
delta.setRelationTuple(rt)
|
||||
return delta
|
||||
}
|
||||
|
||||
const subjectID = new relationTuples.Subject()
|
||||
subjectID.setId("user_charlie")
|
||||
|
||||
const ss = new relationTuples.SubjectSet()
|
||||
ss.setNamespace("User")
|
||||
ss.setObject("charlie")
|
||||
ss.setRelation("")
|
||||
const subjectSet = new relationTuples.Subject()
|
||||
subjectSet.setSet(ss)
|
||||
|
||||
const req = new write.TransactRelationTuplesRequest()
|
||||
req.addRelationTupleDeltas(makeDelta(subjectID))
|
||||
req.addRelationTupleDeltas(makeDelta(subjectSet))
|
||||
|
||||
writeClient.transactRelationTuples(req, (error) => {
|
||||
if (error) {
|
||||
console.log("Encountered error", error)
|
||||
} else {
|
||||
console.log("Successfully created tuples")
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := rts.NewWriteServiceClient(conn)
|
||||
|
||||
// During migration, write every new tuple as both SubjectID and SubjectSet.
|
||||
// This keeps existing SubjectID checks working while the backfill runs.
|
||||
// Here we simulate an incoming request granting Charlie viewer access.
|
||||
_, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: []*rts.RelationTupleDelta{
|
||||
{
|
||||
Action: rts.RelationTupleDelta_ACTION_INSERT,
|
||||
RelationTuple: &rts.RelationTuple{
|
||||
Namespace: "File", Object: "data.txt", Relation: "viewer",
|
||||
Subject: rts.NewSubjectID("user_charlie"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: rts.RelationTupleDelta_ACTION_INSERT,
|
||||
RelationTuple: &rts.RelationTuple{
|
||||
Namespace: "File", Object: "data.txt", Relation: "viewer",
|
||||
Subject: rts.NewSubjectSet("User", "charlie", ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Successfully created tuples")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export KETO_WRITE_REMOTE="127.0.0.1:4467"
|
||||
|
||||
# In production, paginate and filter all SubjectID tuples before writing back.
|
||||
# This script writes the known SubjectSet tuples directly for brevity.
|
||||
printf '%s\n' \
|
||||
'File:data.txt#viewer@User:alice#' \
|
||||
'File:data.txt#viewer@User:bob#' \
|
||||
'File:data.txt#viewer@ApiKey:ci-bot#' | \
|
||||
keto relation-tuple parse -f - --format json | \
|
||||
keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \
|
||||
&& echo "Migration complete" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# In production, paginate GET /admin/relation-tuples?namespace=File&relation=viewer,
|
||||
# filter for tuples with subject_id set, and write SubjectSet counterparts in batches.
|
||||
# This script writes the known SubjectSet tuples directly for brevity.
|
||||
|
||||
curl -X PATCH --fail --silent \
|
||||
-H 'Content-Type: application/json' \
|
||||
--retry 7 --retry-connrefused \
|
||||
--data '[
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"User","object":"alice","relation":""}}},
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"User","object":"bob","relation":""}}},
|
||||
{"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"ApiKey","object":"ci-bot","relation":""}}}
|
||||
]' \
|
||||
http://127.0.0.1:4467/admin/relation-tuples > /dev/null \
|
||||
&& echo "Migration complete" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1 @@
|
||||
Migration complete
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js"
|
||||
import {
|
||||
relationTuples,
|
||||
read,
|
||||
readService,
|
||||
write,
|
||||
writeService,
|
||||
} from "@ory/keto-grpc-client"
|
||||
|
||||
const BATCH_SIZE = 100
|
||||
|
||||
const readClient = new readService.ReadServiceClient(
|
||||
"127.0.0.1:4466",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
const writeClient = new writeService.WriteServiceClient(
|
||||
"127.0.0.1:4467",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
|
||||
function subjectIDToNamespacedSubject(id) {
|
||||
if (id.startsWith("user_"))
|
||||
return { namespace: "User", object: id.slice("user_".length) }
|
||||
if (id.startsWith("apikey_"))
|
||||
return { namespace: "ApiKey", object: id.slice("apikey_".length) }
|
||||
throw new Error("unknown subject ID prefix: " + id)
|
||||
}
|
||||
|
||||
function listRelationTuples(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
readClient.listRelationTuples(req, (error, resp) => {
|
||||
if (error) reject(error)
|
||||
else resolve(resp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function transactRelationTuples(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
writeClient.transactRelationTuples(req, (error, resp) => {
|
||||
if (error) reject(error)
|
||||
else resolve(resp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Step 1: Paginate all File#viewer tuples and collect those with a SubjectID.
|
||||
// In production, persist these to a file or database before writing back,
|
||||
// so the migration can be resumed if interrupted.
|
||||
const toMigrate = []
|
||||
let pageToken = ""
|
||||
do {
|
||||
const req = new read.ListRelationTuplesRequest()
|
||||
const query = new read.ListRelationTuplesRequest.Query()
|
||||
query.setNamespace("File")
|
||||
query.setRelation("viewer")
|
||||
req.setQuery(query)
|
||||
req.setPageSize(BATCH_SIZE)
|
||||
if (pageToken) req.setPageToken(pageToken)
|
||||
|
||||
const resp = await listRelationTuples(req)
|
||||
for (const t of resp.getRelationTuplesList()) {
|
||||
if (t.getSubject().getId()) {
|
||||
toMigrate.push(t)
|
||||
}
|
||||
}
|
||||
pageToken = resp.getNextPageToken()
|
||||
} while (pageToken)
|
||||
|
||||
// Step 2: Write SubjectSet counterparts in batches.
|
||||
for (let i = 0; i < toMigrate.length; i += BATCH_SIZE) {
|
||||
const batch = toMigrate.slice(i, i + BATCH_SIZE)
|
||||
const req = new write.TransactRelationTuplesRequest()
|
||||
|
||||
for (const t of batch) {
|
||||
const { namespace, object } = subjectIDToNamespacedSubject(
|
||||
t.getSubject().getId(),
|
||||
)
|
||||
|
||||
const ss = new relationTuples.SubjectSet()
|
||||
ss.setNamespace(namespace).setObject(object).setRelation("")
|
||||
const sub = new relationTuples.Subject()
|
||||
sub.setSet(ss)
|
||||
|
||||
const rt = new relationTuples.RelationTuple()
|
||||
rt.setNamespace(t.getNamespace())
|
||||
.setObject(t.getObject())
|
||||
.setRelation(t.getRelation())
|
||||
.setSubject(sub)
|
||||
|
||||
const delta = new write.RelationTupleDelta()
|
||||
delta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT)
|
||||
delta.setRelationTuple(rt)
|
||||
req.addRelationTupleDeltas(delta)
|
||||
}
|
||||
|
||||
await transactRelationTuples(req)
|
||||
}
|
||||
|
||||
console.log("Migration complete")
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
||||
)
|
||||
|
||||
const batchSize = 100
|
||||
|
||||
// subjectIDToNamespacedSubject converts a prefixed subject ID to a namespace and object.
|
||||
// The prefix encodes the type: "user_" → User namespace, "apikey_" → ApiKey namespace.
|
||||
func subjectIDToNamespacedSubject(id string) (namespace, object string) {
|
||||
switch {
|
||||
case strings.HasPrefix(id, "user_"):
|
||||
return "User", strings.TrimPrefix(id, "user_")
|
||||
case strings.HasPrefix(id, "apikey_"):
|
||||
return "ApiKey", strings.TrimPrefix(id, "apikey_")
|
||||
default:
|
||||
panic("unknown subject ID prefix: " + id)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
rc, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
wc, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
readClient := rts.NewReadServiceClient(rc)
|
||||
writeClient := rts.NewWriteServiceClient(wc)
|
||||
ctx := context.Background()
|
||||
|
||||
// Step 1: Paginate all File#viewer tuples and collect those with a SubjectID.
|
||||
// In production, persist these to a file, SQLite, or a database before writing back,
|
||||
// so the migration can be resumed if interrupted.
|
||||
var toMigrate []*rts.RelationTuple
|
||||
var pageToken string
|
||||
for {
|
||||
resp, err := readClient.ListRelationTuples(ctx, &rts.ListRelationTuplesRequest{
|
||||
RelationQuery: &rts.RelationQuery{
|
||||
Namespace: new("File"),
|
||||
Relation: new("viewer"),
|
||||
},
|
||||
PageToken: pageToken,
|
||||
PageSize: batchSize,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, t := range resp.RelationTuples {
|
||||
if t.Subject.GetId() != "" {
|
||||
toMigrate = append(toMigrate, t)
|
||||
}
|
||||
}
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
// Step 2: Write SubjectSet counterparts in batches.
|
||||
for i := 0; i < len(toMigrate); i += batchSize {
|
||||
end := min(i+batchSize, len(toMigrate))
|
||||
batch := toMigrate[i:end]
|
||||
|
||||
deltas := make([]*rts.RelationTupleDelta, len(batch))
|
||||
for j, t := range batch {
|
||||
ns, obj := subjectIDToNamespacedSubject(t.Subject.GetId())
|
||||
deltas[j] = &rts.RelationTupleDelta{
|
||||
Action: rts.RelationTupleDelta_ACTION_INSERT,
|
||||
RelationTuple: &rts.RelationTuple{
|
||||
Namespace: t.Namespace,
|
||||
Object: t.Object,
|
||||
Relation: t.Relation,
|
||||
Subject: rts.NewSubjectSet(ns, obj, ""),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := writeClient.TransactRelationTuples(ctx, &rts.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: deltas,
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Migration complete")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export KETO_READ_REMOTE="127.0.0.1:4466"
|
||||
|
||||
keto check User:alice viewer File:data.txt --insecure-disable-transport-security
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
curl -G --silent \
|
||||
--retry 7 --retry-connrefused \
|
||||
--data-urlencode "namespace=File" \
|
||||
--data-urlencode "object=data.txt" \
|
||||
--data-urlencode "relation=viewer" \
|
||||
--data-urlencode "subject_set.namespace=User" \
|
||||
--data-urlencode "subject_set.object=alice" \
|
||||
--data-urlencode "subject_set.relation=" \
|
||||
http://127.0.0.1:4466/relation-tuples/check \
|
||||
| jq -r 'if .allowed == true then "Allowed" else "Denied" end'
|
||||
@@ -0,0 +1 @@
|
||||
Allowed
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js"
|
||||
import { relationTuples, check, checkService } from "@ory/keto-grpc-client"
|
||||
|
||||
const checkClient = new checkService.CheckServiceClient(
|
||||
"127.0.0.1:4466",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
|
||||
const ss = new relationTuples.SubjectSet()
|
||||
ss.setNamespace("User")
|
||||
ss.setObject("alice")
|
||||
ss.setRelation("")
|
||||
const sub = new relationTuples.Subject()
|
||||
sub.setSet(ss)
|
||||
|
||||
const req = new check.CheckRequest()
|
||||
req.setNamespace("File")
|
||||
req.setObject("data.txt")
|
||||
req.setRelation("viewer")
|
||||
req.setSubject(sub)
|
||||
|
||||
checkClient.check(req, (error, resp) => {
|
||||
if (error) {
|
||||
console.log("Encountered error:", error)
|
||||
} else {
|
||||
console.log(resp.getAllowed() ? "Allowed" : "Denied")
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := rts.NewCheckServiceClient(conn)
|
||||
|
||||
res, err := client.Check(context.Background(), &rts.CheckRequest{
|
||||
Namespace: "File",
|
||||
Object: "data.txt",
|
||||
Relation: "viewer",
|
||||
Subject: rts.NewSubjectSet("User", "alice", ""),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if res.Allowed {
|
||||
fmt.Println("Allowed")
|
||||
return
|
||||
}
|
||||
fmt.Println("Denied")
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export KETO_WRITE_REMOTE="127.0.0.1:4467"
|
||||
|
||||
printf '%s\n' \
|
||||
'File:data.txt#viewer@user_alice' \
|
||||
'File:data.txt#viewer@user_bob' \
|
||||
'File:data.txt#viewer@user_charlie' \
|
||||
'File:data.txt#viewer@apikey_ci-bot' | \
|
||||
keto relation-tuple parse -f - --format json | \
|
||||
keto relation-tuple delete -f - >/dev/null --insecure-disable-transport-security \
|
||||
&& echo "Successfully deleted SubjectID tuples" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
curl -X PATCH --fail --silent \
|
||||
-H 'Content-Type: application/json' \
|
||||
--retry 7 --retry-connrefused \
|
||||
--data '[
|
||||
{"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_alice"}},
|
||||
{"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_bob"}},
|
||||
{"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_charlie"}},
|
||||
{"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"apikey_ci-bot"}}
|
||||
]' \
|
||||
http://127.0.0.1:4467/admin/relation-tuples > /dev/null \
|
||||
&& echo "Successfully deleted SubjectID tuples" \
|
||||
|| echo "Encountered error"
|
||||
@@ -0,0 +1 @@
|
||||
Successfully deleted SubjectID tuples
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js"
|
||||
import { relationTuples, write, writeService } from "@ory/keto-grpc-client"
|
||||
|
||||
const writeClient = new writeService.WriteServiceClient(
|
||||
"127.0.0.1:4467",
|
||||
grpc.credentials.createInsecure(),
|
||||
)
|
||||
|
||||
// subjectIdTuples are the SubjectID tuples collected and persisted during the migration step.
|
||||
// In production, load this list from the file or database written during migration so that
|
||||
// you can target deletion of the tuples that were already migrated, and resume if interrupted.
|
||||
const subjectIdTuples = [
|
||||
{
|
||||
namespace: "File",
|
||||
object: "data.txt",
|
||||
relation: "viewer",
|
||||
subjectId: "user_alice",
|
||||
},
|
||||
{
|
||||
namespace: "File",
|
||||
object: "data.txt",
|
||||
relation: "viewer",
|
||||
subjectId: "user_bob",
|
||||
},
|
||||
{
|
||||
namespace: "File",
|
||||
object: "data.txt",
|
||||
relation: "viewer",
|
||||
subjectId: "user_charlie",
|
||||
},
|
||||
{
|
||||
namespace: "File",
|
||||
object: "data.txt",
|
||||
relation: "viewer",
|
||||
subjectId: "apikey_ci-bot",
|
||||
},
|
||||
]
|
||||
|
||||
const req = new write.TransactRelationTuplesRequest()
|
||||
for (const { namespace, object, relation, subjectId } of subjectIdTuples) {
|
||||
const sub = new relationTuples.Subject()
|
||||
sub.setId(subjectId)
|
||||
|
||||
const rt = new relationTuples.RelationTuple()
|
||||
rt.setNamespace(namespace)
|
||||
.setObject(object)
|
||||
.setRelation(relation)
|
||||
.setSubject(sub)
|
||||
|
||||
const delta = new write.RelationTupleDelta()
|
||||
delta.setAction(write.RelationTupleDelta.Action.ACTION_DELETE)
|
||||
delta.setRelationTuple(rt)
|
||||
req.addRelationTupleDeltas(delta)
|
||||
}
|
||||
|
||||
writeClient.transactRelationTuples(req, (error) => {
|
||||
if (error) {
|
||||
console.log("Encountered error", error)
|
||||
} else {
|
||||
console.log("Successfully deleted SubjectID tuples")
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
|
||||
)
|
||||
|
||||
// subjectIdTuples are the SubjectID tuples collected and persisted during migration step.
|
||||
// In production, load this list from the file or database written during migration so that
|
||||
// you can target deletion of the tuples that were already migrated, and resume if interrupted.
|
||||
var subjectIdTuples = []*rts.RelationTuple{
|
||||
{Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("user_alice")},
|
||||
{Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("user_bob")},
|
||||
{Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("user_charlie")},
|
||||
{Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("apikey_ci-bot")},
|
||||
}
|
||||
|
||||
func main() {
|
||||
conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := rts.NewWriteServiceClient(conn)
|
||||
|
||||
deltas := make([]*rts.RelationTupleDelta, len(subjectIdTuples))
|
||||
|
||||
for i, t := range subjectIdTuples {
|
||||
deltas[i] = &rts.RelationTupleDelta{
|
||||
Action: rts.RelationTupleDelta_ACTION_DELETE,
|
||||
RelationTuple: t,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{
|
||||
RelationTupleDeltas: deltas,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Successfully deleted SubjectID tuples")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
dsn: memory
|
||||
log:
|
||||
level: trace
|
||||
|
||||
namespaces:
|
||||
location: file://./subject-id-to-subjectset-migration/namespaces.ts
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Context, Namespace } from "@ory/keto-namespace-types"
|
||||
|
||||
class User implements Namespace {}
|
||||
class ApiKey implements Namespace {}
|
||||
|
||||
class File implements Namespace {
|
||||
related: {
|
||||
viewer: (User | ApiKey)[]
|
||||
}
|
||||
|
||||
permits = {
|
||||
view: (ctx: Context): boolean => this.related.viewer.includes(ctx.subject),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user