forked from baron/baron-sso
org chart 자동로그인 보완. seed-tenant 삭제불가 조치
This commit is contained in:
@@ -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 ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
159
backend/internal/handler/tenant_handler_seed_delete_test.go
Normal file
159
backend/internal/handler/tenant_handler_seed_delete_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user