Files
upstream-mirror 9456a4e922
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
snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:08:34 +05:00

101 lines
2.7 KiB
Go

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