1
0
forked from baron/baron-sso

fix: improve keto sync reliability and initial rebac permissions for super admin

This commit is contained in:
2026-04-06 10:10:27 +09:00
parent bd296f9425
commit 583755c189
11 changed files with 254 additions and 81 deletions

View File

@@ -106,27 +106,39 @@ func (s *ketoService) CheckPermission(ctx context.Context, subject, namespace, o
q.Set("subject_id", subject)
u.RawQuery = q.Encode()
req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
resp, err := s.client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
var lastErr error
maxRetries := 5
backoff := 200 * time.Millisecond
if resp.StatusCode == http.StatusForbidden {
return false, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("keto returned status %d: %s", resp.StatusCode, string(body))
for i := 0; i < maxRetries; i++ {
req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
resp, err := s.client.Do(req)
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var res checkResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return false, err
}
return res.Allowed, nil
}
if resp.StatusCode == http.StatusForbidden {
return false, nil
}
body, _ := io.ReadAll(resp.Body)
lastErr = fmt.Errorf("keto returned status %d: %s", resp.StatusCode, string(body))
} else {
lastErr = err
}
if i < maxRetries-1 {
slog.Debug("Retrying Keto CheckPermission...", "attempt", i+1, "error", lastErr)
time.Sleep(backoff)
backoff *= 2
}
}
var res checkResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return false, err
}
return res.Allowed, nil
return false, lastErr
}
func (s *ketoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
@@ -141,8 +153,8 @@ func (s *ketoService) CreateRelation(ctx context.Context, namespace, object, rel
// Exponential Backoff Retry Logic
var lastErr error
maxRetries := 3
backoff := 100 * time.Millisecond
maxRetries := 5
backoff := 200 * time.Millisecond
for i := 0; i < maxRetries; i++ {
req, _ := http.NewRequestWithContext(ctx, "PUT", u, bytes.NewReader(body))
@@ -152,7 +164,7 @@ func (s *ketoService) CreateRelation(ctx context.Context, namespace, object, rel
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
slog.Info("Keto relation created", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
slog.Debug("Keto relation created", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
return nil
}
resBody, _ := io.ReadAll(resp.Body)
@@ -161,11 +173,14 @@ func (s *ketoService) CreateRelation(ctx context.Context, namespace, object, rel
lastErr = err
}
time.Sleep(backoff)
backoff *= 2
if i < maxRetries-1 {
slog.Debug("Retrying Keto CreateRelation...", "attempt", i+1, "error", lastErr)
time.Sleep(backoff)
backoff *= 2
}
}
slog.Error("Keto create relation failed after retries", "error", lastErr)
slog.Error("Keto create relation failed after retries", "error", lastErr, "namespace", namespace, "object", object, "relation", relation, "subject", subject)
return lastErr
}
@@ -178,20 +193,34 @@ func (s *ketoService) DeleteRelation(ctx context.Context, namespace, object, rel
q.Set("subject_id", subject)
u.RawQuery = q.Encode()
req, _ := http.NewRequestWithContext(ctx, "DELETE", u.String(), nil)
resp, err := s.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var lastErr error
maxRetries := 5
backoff := 200 * time.Millisecond
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
resBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("keto returned status %d: %s", resp.StatusCode, string(resBody))
for i := 0; i < maxRetries; i++ {
req, _ := http.NewRequestWithContext(ctx, "DELETE", u.String(), nil)
resp, err := s.client.Do(req)
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
slog.Debug("Keto relation deleted", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
return nil
}
resBody, _ := io.ReadAll(resp.Body)
lastErr = fmt.Errorf("keto returned status %d: %s", resp.StatusCode, string(resBody))
} else {
lastErr = err
}
if i < maxRetries-1 {
slog.Debug("Retrying Keto DeleteRelation...", "attempt", i+1, "error", lastErr)
time.Sleep(backoff)
backoff *= 2
}
}
slog.Info("Keto relation deleted", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
return nil
slog.Error("Keto delete relation failed after retries", "error", lastErr)
return lastErr
}
func (s *ketoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {

View File

@@ -120,6 +120,15 @@ func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, tenantTy
// [Keto] Sync hierarchy and ownership via Outbox
if s.outboxRepo != nil {
// Global Super Admin access to every tenant
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
Namespace: "Tenant",
Object: tenant.ID,
Relation: "admins",
Subject: "System:global#super_admins",
Action: domain.KetoOutboxActionCreate,
})
// Sync hierarchy
if tenant.ParentID != nil {
if err := s.outboxRepo.Create(ctx, &domain.KetoOutbox{
@@ -198,6 +207,17 @@ func (s *tenantService) RequestRegistration(ctx context.Context, name, slug, des
return nil, err
}
// [Keto] Global Super Admin access to every tenant (even pending ones)
if s.outboxRepo != nil {
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
Namespace: "Tenant",
Object: tenant.ID,
Relation: "admins",
Subject: "System:global#super_admins",
Action: domain.KetoOutboxActionCreate,
})
}
// Add Domain as unverified
if err := s.repo.AddDomain(ctx, tenant.ID, domainName, false); err != nil {
return nil, err