1
0
forked from baron/baron-sso

org chart 자동로그인 보완. seed-tenant 삭제불가 조치

This commit is contained in:
2026-04-30 17:02:24 +09:00
parent 6eb4c293ff
commit 3dcdd97882
13 changed files with 490 additions and 32 deletions

View File

@@ -7420,6 +7420,9 @@ func resolveLinkedRPAutoLoginSupported(clientID string, metadata map[string]inte
func resolveLinkedRPAutoLoginURL(clientID string, metadata map[string]interface{}) string {
clientID = strings.TrimSpace(clientID)
if metadataURL := readMetadataStringValue(metadata, domain.MetadataAutoLoginURL); metadataURL != "" {
if clientID == "orgfront" {
return ensureOrgfrontAutoLoginURL(metadataURL)
}
return metadataURL
}
@@ -7434,13 +7437,29 @@ func resolveLinkedRPAutoLoginURL(clientID string, metadata map[string]interface{
}
case "orgfront":
if value := strings.TrimRight(strings.TrimSpace(os.Getenv("ORGFRONT_URL")), "/"); value != "" {
return value + "/login"
return value + "/login?auto=1"
}
}
return ""
}
func ensureOrgfrontAutoLoginURL(rawURL string) string {
parsed, err := url.Parse(rawURL)
if err != nil {
return rawURL
}
if strings.TrimRight(parsed.Path, "/") != "/login" {
return rawURL
}
query := parsed.Query()
if query.Get("auto") != "1" {
query.Set("auto", "1")
parsed.RawQuery = query.Encode()
}
return parsed.String()
}
func resolveLinkedRPInitURL(clientID string, metadata map[string]interface{}) string {
if !resolveLinkedRPAutoLoginSupported(clientID, metadata) {
return ""

View File

@@ -197,7 +197,7 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
}
}
assert.True(t, orgfrontItem.AutoLoginSupported)
assert.Equal(t, "http://localhost:5175/login", orgfrontItem.AutoLoginURL)
assert.Equal(t, "http://localhost:5175/login?auto=1", orgfrontItem.AutoLoginURL)
assert.Equal(t, orgfrontItem.AutoLoginURL, orgfrontItem.InitURL)
}

View File

@@ -1,6 +1,7 @@
package handler
import (
"baron-sso-backend/internal/bootstrap"
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
@@ -28,6 +29,23 @@ type TenantHandler struct {
SharedLink service.SharedLinkService
}
func seedTenantDeleteError(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusConflict, "seed tenants cannot be deleted")
}
func seedTenantSlugsForDeleteGuard() []string {
slugs, err := bootstrap.SeedTenantSlugSet()
if err != nil {
return nil
}
result := make([]string, 0, len(slugs))
for slug := range slugs {
result = append(result, slug)
}
return result
}
func NewTenantHandler(db *gorm.DB, svc service.TenantService, userRepo repository.UserRepository, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService, sharedLink service.SharedLinkService) *TenantHandler {
return &TenantHandler{
DB: db,
@@ -1045,7 +1063,6 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
}
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
var req struct {
Name *string `json:"name"`
Type *string `json:"type"`
@@ -1192,6 +1209,9 @@ func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
}
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if bootstrap.IsSeedTenantSlug(tenant.Slug) {
return seedTenantDeleteError(c)
}
// Rename slug to release it for reuse before soft delete
deletedSlug := tenant.Slug + "-deleted-" + time.Now().Format("20060102150405")
@@ -1502,6 +1522,20 @@ func (h *TenantHandler) DeleteTenantsBulk(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusForbidden, "only super admin can perform bulk deletion")
}
protectedSlugs := seedTenantSlugsForDeleteGuard()
if len(protectedSlugs) > 0 {
var protectedCount int64
if err := h.DB.Model(&domain.Tenant{}).
Where("id IN ?", req.IDs).
Where("slug IN ?", protectedSlugs).
Count(&protectedCount).Error; err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if protectedCount > 0 {
return seedTenantDeleteError(c)
}
}
if err := h.Service.DeleteTenantsBulk(c.Context(), req.IDs); err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}

View File

@@ -0,0 +1,159 @@
package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/testsupport"
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/testcontainers/testcontainers-go"
postgres_module "github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
gorm_postgres "gorm.io/driver/postgres"
"gorm.io/gorm"
)
func newTenantHandlerSeedDeleteDB(t *testing.T) *gorm.DB {
t.Helper()
if !testsupport.DockerAvailable() {
t.Skip("Docker provider is unavailable in this environment")
}
ctx := context.Background()
postgresContainer, err := postgres_module.Run(ctx,
"postgres:16-alpine",
postgres_module.WithDatabase("testdb"),
postgres_module.WithUsername("user"),
postgres_module.WithPassword("password"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(30*time.Second)),
)
if err != nil {
t.Fatalf("failed to start postgres container: %v", err)
}
t.Cleanup(func() {
if err := postgresContainer.Terminate(ctx); err != nil {
log.Printf("failed to terminate postgres container: %v", err)
}
})
connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable")
if err != nil {
t.Fatalf("failed to get postgres connection string: %v", err)
}
db, err := gorm.Open(gorm_postgres.Open(connStr), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open postgres connection: %v", err)
}
if err := db.AutoMigrate(&domain.Tenant{}); err != nil {
t.Fatalf("failed to migrate tenants: %v", err)
}
return db
}
func setSeedTenantCSVForDeleteGuard(t *testing.T, slug string) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "seed-tenant.csv")
csv := "name,type,parent_tenant_slug,slug,memo,email_domain\n" +
"Protected,COMPANY_GROUP,," + slug + ",Protected seed,\n"
if err := os.WriteFile(path, []byte(csv), 0o600); err != nil {
t.Fatalf("failed to write seed csv: %v", err)
}
t.Setenv("SEED_TENANT_CSV_PATH", path)
}
func TestTenantHandlerDeleteTenantRejectsSeedTenant(t *testing.T) {
setSeedTenantCSVForDeleteGuard(t, "protected-root")
db := newTenantHandlerSeedDeleteDB(t)
tenant := domain.Tenant{
ID: "00000000-0000-0000-0000-000000000001",
Name: "Protected",
Slug: "protected-root",
Type: domain.TenantTypeCompanyGroup,
Status: domain.TenantStatusActive,
}
if err := db.Create(&tenant).Error; err != nil {
t.Fatalf("failed to create tenant: %v", err)
}
app := fiber.New()
app.Delete("/tenants/:id", (&TenantHandler{DB: db}).DeleteTenant)
req := httptest.NewRequest(http.MethodDelete, "/tenants/"+tenant.ID, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
if resp.StatusCode != http.StatusConflict {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusConflict)
}
var count int64
if err := db.Model(&domain.Tenant{}).Where("id = ?", tenant.ID).Count(&count).Error; err != nil {
t.Fatalf("count tenant: %v", err)
}
if count != 1 {
t.Fatalf("seed tenant count = %d, want 1", count)
}
}
func TestTenantHandlerDeleteTenantsBulkRejectsSeedTenant(t *testing.T) {
setSeedTenantCSVForDeleteGuard(t, "protected-root")
db := newTenantHandlerSeedDeleteDB(t)
seed := domain.Tenant{
ID: "00000000-0000-0000-0000-000000000011",
Name: "Protected",
Slug: "protected-root",
Type: domain.TenantTypeCompanyGroup,
Status: domain.TenantStatusActive,
}
normal := domain.Tenant{
ID: "00000000-0000-0000-0000-000000000012",
Name: "Normal",
Slug: "normal",
Type: domain.TenantTypeCompany,
Status: domain.TenantStatusActive,
}
if err := db.Create(&seed).Error; err != nil {
t.Fatalf("failed to create seed tenant: %v", err)
}
if err := db.Create(&normal).Error; err != nil {
t.Fatalf("failed to create normal tenant: %v", err)
}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
return c.Next()
})
app.Delete("/tenants/bulk", (&TenantHandler{DB: db}).DeleteTenantsBulk)
body, _ := json.Marshal(map[string][]string{"ids": []string{seed.ID, normal.ID}})
req := httptest.NewRequest(http.MethodDelete, "/tenants/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
if resp.StatusCode != http.StatusConflict {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusConflict)
}
var count int64
if err := db.Model(&domain.Tenant{}).Where("id IN ?", []string{seed.ID, normal.ID}).Count(&count).Error; err != nil {
t.Fatalf("count tenants: %v", err)
}
if count != 2 {
t.Fatalf("remaining tenant count = %d, want 2", count)
}
}