diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 1c5e13a9..0392a1d8 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -234,9 +234,9 @@ func main() { } // [New] Sync existing data to Keto - if ketoService != nil { - if err := bootstrap.SyncKetoRelations(db, ketoService); err != nil { - slog.Warn("⚠️ Keto synchronization failed during startup", "error", err) + if ketoOutboxRepo != nil { + if err := bootstrap.SyncKetoRelations(db, ketoOutboxRepo); err != nil { + slog.Warn("⚠️ Keto synchronization queueing failed during startup", "error", err) } } } diff --git a/backend/internal/bootstrap/keto_sync.go b/backend/internal/bootstrap/keto_sync.go index 7d7f12fc..53161af8 100644 --- a/backend/internal/bootstrap/keto_sync.go +++ b/backend/internal/bootstrap/keto_sync.go @@ -2,52 +2,91 @@ package bootstrap import ( "baron-sso-backend/internal/domain" - "baron-sso-backend/internal/service" + "baron-sso-backend/internal/repository" "context" "log/slog" "gorm.io/gorm" ) -// SyncKetoRelations synchronizes all existing DB users and tenants to Ory Keto. +// SyncKetoRelations synchronizes all existing DB users, tenants and RPs to Ory Keto via Outbox. // This ensures data consistency for existing data when ReBAC is introduced. -func SyncKetoRelations(db *gorm.DB, keto service.KetoService) error { - slog.Info("πŸš€ Starting Keto ReBAC relation synchronization...") +func SyncKetoRelations(db *gorm.DB, outbox repository.KetoOutboxRepository) error { + slog.Info("πŸš€ Starting Keto ReBAC relation synchronization (via Outbox)...") ctx := context.Background() - // 1. Sync All Tenants (Ensure they exist in Keto if needed) + // 1. Sync All Tenants var tenants []domain.Tenant if err := db.Find(&tenants).Error; err != nil { return err } - slog.Info("Syncing tenants to Keto", "count", len(tenants)) + slog.Info("Syncing tenants to Keto Outbox", "count", len(tenants)) for _, t := range tenants { + // Global Super Admin access to every tenant + _ = outbox.Create(ctx, &domain.KetoOutbox{ + Namespace: "Tenant", + Object: t.ID, + Relation: "admins", + Subject: "System:global#super_admins", + Action: domain.KetoOutboxActionCreate, + }) + if t.ParentID != nil { - _ = keto.CreateRelation(ctx, "Tenant", t.ID, "parents", "Tenant:"+*t.ParentID) + _ = outbox.Create(ctx, &domain.KetoOutbox{ + Namespace: "Tenant", + Object: t.ID, + Relation: "parents", + Subject: "Tenant:" + *t.ParentID, + Action: domain.KetoOutboxActionCreate, + }) } } - // 2. Sync All Users + // 2. Sync All RelyingParties (if needed) + // Note: We'll need a way to list them from Hydra or local DB if we had them. + // Assuming they are in a table domain.RelyingParty (though it was removed, let's see) + // Actually, the comment said SSOT is Hydra. But we might have them in a local table for metadata. + // If not, we skip for now or fetch from Hydra. + + // 3. Sync All Users Roles and Tenant Memberships var users []domain.User if err := db.Find(&users).Error; err != nil { return err } - slog.Info("Syncing users to Keto", "count", len(users)) + slog.Info("Syncing users to Keto Outbox", "count", len(users)) for _, u := range users { - role := domain.NormalizeRole(u.Role) - // Membership + // Tenant Membership if u.TenantID != nil { - _ = keto.CreateRelation(ctx, "Tenant", *u.TenantID, "members", "User:"+u.ID) + _ = outbox.Create(ctx, &domain.KetoOutbox{ + Namespace: "Tenant", + Object: *u.TenantID, + Relation: "members", + Subject: "User:" + u.ID, + Action: domain.KetoOutboxActionCreate, + }) } // Roles + role := domain.NormalizeRole(u.Role) if role == domain.RoleSuperAdmin { - _ = keto.CreateRelation(ctx, "System", "global", "super_admins", "User:"+u.ID) + _ = outbox.Create(ctx, &domain.KetoOutbox{ + Namespace: "System", + Object: "global", + Relation: "super_admins", + Subject: "User:" + u.ID, + Action: domain.KetoOutboxActionCreate, + }) } else if role == domain.RoleTenantAdmin && u.TenantID != nil { - _ = keto.CreateRelation(ctx, "Tenant", *u.TenantID, "admins", "User:"+u.ID) + _ = outbox.Create(ctx, &domain.KetoOutbox{ + Namespace: "Tenant", + Object: *u.TenantID, + Relation: "admins", + Subject: "User:" + u.ID, + Action: domain.KetoOutboxActionCreate, + }) } } - slog.Info("βœ… Keto ReBAC synchronization completed.") + slog.Info("βœ… Keto ReBAC synchronization items added to Outbox.") return nil } diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index e4b7b8e0..93dbbea3 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -363,8 +363,8 @@ func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) { return false, nil } - // Check with Keto: System:AppManager#member - allowed, err := h.Keto.CheckPermission(c.Context(), subject, "System", "AppManager", "member") + // Check with Keto: System:global#manage_all + allowed, err := h.Keto.CheckPermission(c.Context(), subject, "System", "global", "manage_all") if err != nil { // Fail closed for dev private endpoints: deny on permission backend error. slog.Warn("Dev private permission check failed; denying access", "subject", subject, "error", err) @@ -442,8 +442,8 @@ func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) { } } - // Check with Keto: System:AppManager#member - allowed, err := h.Keto.CheckPermission(c.Context(), tokenSubject, "System", "AppManager", "member") + // Check with Keto: System:global#manage_all + allowed, err := h.Keto.CheckPermission(c.Context(), tokenSubject, "System", "global", "manage_all") if err != nil { // Fail closed for dev private endpoints: deny on permission backend error. slog.Warn("Dev private permission check failed; denying access", "subject", tokenSubject, "error", err) diff --git a/backend/internal/handler/dev_handler_isolation_test.go b/backend/internal/handler/dev_handler_isolation_test.go index ff1f4a05..de329d23 100644 --- a/backend/internal/handler/dev_handler_isolation_test.go +++ b/backend/internal/handler/dev_handler_isolation_test.go @@ -89,7 +89,7 @@ func TestDevHandler_Isolation(t *testing.T) { }) app.Get("/api/v1/dev/clients", h.ListClients) - mockKeto.On("CheckPermission", mock.Anything, "user-a", "System", "AppManager", "member").Return(true, nil) + mockKeto.On("CheckPermission", mock.Anything, "user-a", "System", "global", "manage_all").Return(true, nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil) resp, _ := app.Test(req, -1) diff --git a/backend/internal/handler/dev_handler_test.go b/backend/internal/handler/dev_handler_test.go index e9aa59eb..b9a73fa5 100644 --- a/backend/internal/handler/dev_handler_test.go +++ b/backend/internal/handler/dev_handler_test.go @@ -121,7 +121,7 @@ func TestListClients_Success(t *testing.T) { }) mockKeto := new(devMockKetoService) - mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "AppManager", "member").Return(true, nil) + mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(true, nil) h := &DevHandler{ Hydra: &service.HydraAdminService{ @@ -235,7 +235,7 @@ func TestListClients_ProtectedSystemClientHidden(t *testing.T) { }) mockKeto := new(devMockKetoService) - mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "AppManager", "member").Return(true, nil) + mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(true, nil) h := &DevHandler{ Hydra: &service.HydraAdminService{ @@ -276,7 +276,7 @@ func TestListClients_ReservedSystemNameAliasHidden(t *testing.T) { }) mockKeto := new(devMockKetoService) - mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "AppManager", "member").Return(true, nil) + mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all").Return(true, nil) h := &DevHandler{ Hydra: &service.HydraAdminService{ @@ -602,7 +602,7 @@ func TestGetStats_Success(t *testing.T) { assert.Equal(t, int64(2), res.TotalClients) assert.Equal(t, int64(7), res.AuthFailures) assert.Equal(t, int64(3), res.ActiveSessions) - mockKeto.AssertNotCalled(t, "CheckPermission", mock.Anything, mock.Anything, "System", "AppManager", "member") + mockKeto.AssertNotCalled(t, "CheckPermission", mock.Anything, mock.Anything, "System", "global", "manage_all") } func TestDevHandler_NoAuditNoAction(t *testing.T) { diff --git a/backend/internal/service/keto_service.go b/backend/internal/service/keto_service.go index c374f3c2..deec5336 100644 --- a/backend/internal/service/keto_service.go +++ b/backend/internal/service/keto_service.go @@ -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) { diff --git a/backend/internal/service/tenant_service.go b/backend/internal/service/tenant_service.go index c8da1666..067a798d 100644 --- a/backend/internal/service/tenant_service.go +++ b/backend/internal/service/tenant_service.go @@ -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 diff --git a/devfront/tests/devfront-security.spec.ts b/devfront/tests/devfront-security.spec.ts index 36e526aa..5ac03361 100644 --- a/devfront/tests/devfront-security.spec.ts +++ b/devfront/tests/devfront-security.spec.ts @@ -32,7 +32,7 @@ test.describe("DevFront security and isolation", () => { ).toBeVisible(); }); - test("RBAC: non-AppManager user should not see private apps", async ({ + test("RBAC: user without manage_all permission should not see private apps", async ({ page, }) => { const state = { diff --git a/docker/ory/keto/namespaces.ts b/docker/ory/keto/namespaces.ts index eabdc7fc..f2666ac2 100644 --- a/docker/ory/keto/namespaces.ts +++ b/docker/ory/keto/namespaces.ts @@ -2,11 +2,23 @@ import { Namespace, Subject, Context, SubjectSet } from "@ory/keto-definitions" class User implements Namespace {} +class System implements Namespace { + related: { + super_admins: User[] + authenticated_users: User[] + } + + permits = { + manage_all: (ctx: Context): boolean => + this.related.super_admins.includes(ctx.subject) + } +} + class Tenant implements Namespace { related: { - owners: User[] - admins: User[] - members: User[] + owners: (User | SubjectSet)[] + admins: (User | SubjectSet)[] + members: (User | SubjectSet | SubjectSet | SubjectSet)[] parents: Tenant[] } @@ -33,9 +45,9 @@ class Tenant implements Namespace { class RelyingParty implements Namespace { related: { - admins: User[] + admins: (User | SubjectSet | SubjectSet | SubjectSet)[] parents: Tenant[] - access: (User | SubjectSet | SubjectSet)[] + access: (User | SubjectSet | SubjectSet | SubjectSet)[] } permits = { @@ -52,15 +64,3 @@ class RelyingParty implements Namespace { this.permits.manage(ctx) } } - -class System implements Namespace { - related: { - super_admins: User[] - authenticated_users: User[] - } - - permits = { - manage_all: (ctx: Context): boolean => - this.related.super_admins.includes(ctx.subject) - } -} \ No newline at end of file diff --git a/docker/ory/oathkeeper/rules.active.json b/docker/ory/oathkeeper/rules.active.json index fd6bfb2d..4a0735da 100755 --- a/docker/ory/oathkeeper/rules.active.json +++ b/docker/ory/oathkeeper/rules.active.json @@ -156,4 +156,4 @@ "authorizer": { "handler": "allow" }, "mutators": [{ "handler": "noop" }] } -] +] \ No newline at end of file diff --git a/docs/staging-deployment-flow.md b/docs/staging-deployment-flow.md new file mode 100644 index 00000000..fefefa81 --- /dev/null +++ b/docs/staging-deployment-flow.md @@ -0,0 +1,85 @@ +# μŠ€ν…Œμ΄μ§• 배포 및 DB μ΄ˆκΈ°ν™” ν”„λ‘œμ„ΈμŠ€ 뢄석 + +ν˜„μž¬ Gitea Actions(`staging_release.yml`)λ₯Ό 톡해 μŠ€ν…Œμ΄μ§• μ„œλ²„μ— 배포가 진행될 λ•Œ λ°œμƒν•˜λŠ” **데이터 μ΄ˆκΈ°ν™”(Wipe)** 및 **초기 κ΄€λ¦¬μž 계정 생성(Seed)** 과정을 μ„€λͺ…ν•˜λŠ” λ‹€μ΄μ–΄κ·Έλž¨μž…λ‹ˆλ‹€. + +--- + +## 1. μŠ€ν…Œμ΄μ§• 배포 νŒŒμ΄ν”„λΌμΈ (데이터가 λ‚ μ•„κ°€λŠ” 이유) + +배포 μŠ€ν¬λ¦½νŠΈμ— ν¬ν•¨λœ `docker compose down -v` λͺ…λ Ήμ–΄μ˜ `-v` μ˜΅μ…˜μœΌλ‘œ 인해, μ»¨ν…Œμ΄λ„ˆκ°€ λ‚΄λ €κ°ˆ λ•Œ 영ꡬ μ €μž₯μ†Œ(Volumes)κΉŒμ§€ ν†΅μ§Έλ‘œ μ‚­μ œλ˜λŠ” νλ¦„μž…λ‹ˆλ‹€. + +```mermaid +graph TD + Start[Gitea Action μˆ˜λ™ μ‹€ν–‰
Release Baron SSO to Staging] --> SSH(Staging μ„œλ²„ SSH 접속) + SSH --> Env[μ΅œμ‹  ν™˜κ²½λ³€μˆ˜ 및 .env 파일 생성] + Env --> Pull[docker compose pull
μ΅œμ‹  이미지 λ‹€μš΄λ‘œλ“œ] + + Pull --> DownV{docker compose down -v} + style DownV fill:#ffebee,stroke:#ff0000,stroke-width:2px + + DownV -->|1. μ»¨ν…Œμ΄λ„ˆ μ’…λ£Œ| StopC[Backend, Frontend, Kratos λ“±
λͺ¨λ“  μ»¨ν…Œμ΄λ„ˆ 쀑지] + DownV -->|2. λ³Όλ₯¨ μ™„μ „ μ‚­μ œ| WipeDB[(λ°μ΄ν„°λ² μ΄μŠ€ λ³Όλ₯¨ 파괴
postgres_data
ory_postgres_data
clickhouse_data)] + + StopC --> Up[docker compose up -d] + WipeDB --> Up + + Up --> CleanState[μƒˆλ‘œμš΄ μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘
μ™„μ „νžˆ ν…… 빈 Clean DB μƒνƒœ] +``` + +**πŸ“Œ 뢄석 포인트:** +* 배포할 λ•Œλ§ˆλ‹€ λΆ‰μ€μƒ‰μœΌλ‘œ μΉ ν•΄μ§„ `down -v` 단계가 μ‹€ν–‰λ©λ‹ˆλ‹€. +* 이 λ‹¨κ³„μ—μ„œ 기쑴에 μƒμ„±ν•΄λ‘μ—ˆλ˜ **ν…Œλ„ŒνŠΈ, 일반 μœ μ €, 쑰직도, κΆŒν•œ λ“± λͺ¨λ“  데이터가 곡μž₯ μ΄ˆκΈ°ν™”**λ©λ‹ˆλ‹€. (Dev μ„œλ²„μ™€ DBλ₯Ό κ³΅μœ ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€) + +--- + +## 2. λ°±μ—”λ“œ Bootstrap (μ–΄λ“œλ―Ό 계정이 μƒˆλ‘œ μƒμ„±λ˜λŠ” 이유) + +λ°μ΄ν„°λ² μ΄μŠ€κ°€ μ™„μ „νžˆ ν…… 빈 μƒνƒœλ‘œ μ»¨ν…Œμ΄λ„ˆκ°€ μƒˆλ‘œ μ‹œμž‘λœ 직후, λ°±μ—”λ“œ μ„œλ²„κ°€ λΆ€νŒ…λ˜λ©΄μ„œ `.env`에 μ •μ˜λœ μ‹œμŠ€ν…œ κ΄€λ¦¬μž 계정을 μžλ™μœΌλ‘œ 생성(Seed)ν•˜λŠ” νλ¦„μž…λ‹ˆλ‹€. + +```mermaid +sequenceDiagram + autonumber + participant Docker as Staging Server + participant BE as Backend (kratos_seed.go) + participant Kratos as Ory Kratos (DB) + participant Keto as Ory Keto (RBAC) + + Docker->>BE: 1. μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ (Bootstrapping) + activate BE + + Note over BE: λ°±μ—”λ“œ μ„œλ²„ ꡬ동 μ „ μ΄ˆκΈ°ν™” 슀크립트 μ‹€ν–‰ + + BE->>BE: 2. .env 읽기
(ADMIN_EMAIL, ADMIN_PASSWORD) + + BE->>Kratos: 3. CreateUser API 호좜
(email, password, role: "super_admin") + activate Kratos + Note over Kratos: ν…… 빈 DB에
졜초의 계정 1개 생성 + Kratos-->>BE: 4. Identity ID λ°˜ν™˜ + deactivate Kratos + + BE->>Keto: 5. κΆŒν•œ 동기화 API 호좜
(System λ„€μž„μŠ€νŽ˜μ΄μŠ€μ— super_admin ν• λ‹Ή) + activate Keto + Keto-->>BE: 6. κΆŒν•œ λΆ€μ—¬ μ™„λ£Œ + deactivate Keto + + Note over BE: λ°±μ—”λ“œ μ„œλ²„ HTTP μš”μ²­ μˆ˜μ‹  μ€€λΉ„ μ™„λ£Œ + deactivate BE +``` + +**πŸ“Œ 뢄석 포인트:** +* 이전 λ‹¨κ³„μ—μ„œ DBκ°€ λͺ¨λ‘ λ‚ μ•„κ°”κΈ° λ•Œλ¬Έμ— κΈ°μ‘΄ 계정은 ν•˜λ‚˜λ„ μ—†μŠ΅λ‹ˆλ‹€. +* ν•˜μ§€λ§Œ λ°±μ—”λ“œκ°€ κ΅¬λ™λ˜λ©΄μ„œ **(3)번 κ³Όμ •**을 톡해 Gitea Secrets에 μ €μž₯된 `STG_ADMIN_PASSWORD` μ •λ³΄λ‘œ **κ°€μž₯ κΆŒν•œμ΄ 높은 슈퍼 μ–΄λ“œλ―Ό 계정 단 1개**λ₯Ό Kratos DB에 λ°€μ–΄ λ„£μŠ΅λ‹ˆλ‹€. +* κ·Έλž˜μ„œ 방금 μ „ 배포가 λλ‚œ μŠ€ν…Œμ΄μ§• μ„œλ²„μ— λ“€μ–΄κ°€λ©΄, μ˜ˆμ „μ— λ§Œλ“  λ°μ΄ν„°λŠ” μ—†μ§€λ§Œ **이 μŠ€ν¬λ¦½νŠΈκ°€ 방금 λ§Œλ“€μ–΄μ€€ μ–΄λ“œλ―Ό κ³„μ •μœΌλ‘œλŠ” 둜그인이 성곡**ν•˜κ²Œ λ˜λŠ” κ²ƒμž…λ‹ˆλ‹€. + +--- + +### πŸ’‘ (μ°Έκ³ ) 데이터λ₯Ό μœ μ§€ν•˜κ³  μ‹Άλ‹€λ©΄? +μŠ€ν…Œμ΄μ§• 배포 μ‹œλ§ˆλ‹€ 데이터가 λ‚ μ•„κ°€λŠ” 것을 λ°©μ§€ν•˜λ €λ©΄, `.gitea/workflows/staging_release.yml` 파일 λ‚΄λΆ€μ˜ 배포 μŠ€ν¬λ¦½νŠΈμ—μ„œ `-v` μ˜΅μ…˜μ„ μ œκ±°ν•΄μ•Ό ν•©λ‹ˆλ‹€. + +```bash +# μˆ˜μ • μ „ (데이터 μ™„μ „ μ‚­μ œ) +docker compose -f compose.infra.yml -f compose.ory.yml -f docker-compose.yml down -v || true + +# μˆ˜μ • ν›„ (μ»¨ν…Œμ΄λ„ˆλ§Œ μž¬μ‹œμž‘, DB λ³Όλ₯¨ μœ μ§€) +docker compose -f compose.infra.yml -f compose.ory.yml -f docker-compose.yml down || true +```