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

View File

@@ -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"

View File

@@ -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"

View File

@@ -0,0 +1 @@
Successfully created tuples

View File

@@ -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")
}
})

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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'

View File

@@ -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")
}
})

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -0,0 +1 @@
Successfully created tuples

View File

@@ -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")
}
})

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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")

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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'

View File

@@ -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")
}
})

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -0,0 +1 @@
Successfully deleted SubjectID tuples

View File

@@ -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")
}
})

View File

@@ -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")
}

View File

@@ -0,0 +1,6 @@
dsn: memory
log:
level: trace
namespaces:
location: file://./subject-id-to-subjectset-migration/namespaces.ts

View File

@@ -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),
}
}