1
0
forked from baron/baron-sso

Merge branch 'feature/worksmobile' into dev

This commit is contained in:
2026-05-06 09:31:04 +09:00
74 changed files with 8698 additions and 212 deletions

View File

@@ -16,6 +16,7 @@ import (
"log/slog"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -39,6 +40,34 @@ func getEnv(key, fallback string) string {
return fallback
}
func getEnvFileOrValue(fileKey string, valueKey string, fallback string) (string, error) {
if path := strings.TrimSpace(getEnv(fileKey, "")); path != "" {
value, err := readEnvFileValue(path)
if err != nil {
return "", err
}
return value, nil
}
return getEnv(valueKey, fallback), nil
}
func readEnvFileValue(path string) (string, error) {
candidates := []string{path}
if !filepath.IsAbs(path) {
candidates = append(candidates, filepath.Join("..", path), filepath.Join("..", "..", path))
}
var lastErr error
for _, candidate := range candidates {
data, err := os.ReadFile(candidate)
if err == nil {
return string(data), nil
}
lastErr = err
}
return "", fmt.Errorf("read secret file %q: %w", path, lastErr)
}
func normalizeDocsPrefix(prefix string) string {
trimmed := strings.TrimSpace(prefix)
if trimmed == "" || trimmed == "/" {
@@ -268,11 +297,32 @@ func main() {
userGroupRepo := repository.NewUserGroupRepository(db)
userRepo := repository.NewUserRepository(db)
ketoOutboxRepo := repository.NewKetoOutboxRepository(db) // Reuse or re-init
worksmobileOutboxRepo := repository.NewWorksmobileOutboxRepository(db)
sharedLinkRepo := repository.NewSharedLinkRepository(db)
kratosAdminService := service.NewKratosAdminService()
oryAdminProvider := service.NewOryProvider()
tenantService := service.NewTenantService(tenantRepo, userRepo, userGroupRepo, ketoOutboxRepo)
worksmobilePrivateKey, err := getEnvFileOrValue("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE", "WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY", "")
if err != nil {
slog.Error("Worksmobile private key file could not be loaded", "error", err)
os.Exit(1)
}
worksmobileClient := service.NewWorksmobileHTTPClientWithAuth(
getEnv("WORKS_ADMIN_ACCESS_TOKEN", getEnv("WORKS_ADMIN_OAUTH_ACCESS_TOKEN", "")),
getEnv("SAMAN_SCIM_LONGLIVE_TOKEN", ""),
service.WorksmobileOAuthConfig{
ClientID: getEnv("WORKS_ADMIN_OAUTH_CLIENT_ID", ""),
ClientSecret: getEnv("WORKS_ADMIN_OAUTH_CLIENT_SECRET", ""),
ServiceAccount: getEnv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT", ""),
PrivateKey: worksmobilePrivateKey,
Scope: getEnv("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
},
)
worksmobileService := service.NewWorksmobileSyncService(tenantService, userRepo, worksmobileOutboxRepo, worksmobileClient)
worksmobileRelayWorker := service.NewWorksmobileRelayWorker(worksmobileOutboxRepo, worksmobileClient)
go worksmobileRelayWorker.Start(context.Background())
slog.Info("✅ Worksmobile Relay Worker started")
sharedLinkService := service.NewSharedLinkService(sharedLinkRepo)
userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, tenantRepo, ketoService, ketoOutboxRepo, kratosAdminService)
tenantService.SetKetoService(ketoService) // Keto 주입
@@ -301,6 +351,9 @@ func main() {
userGroupHandler := handler.NewUserGroupHandler(userGroupService)
relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, ketoService, ketoOutboxRepo, userRepo, userGroupRepo, auditRepo)
tenantHandler.SetWorksmobileSyncer(worksmobileService)
userHandler.SetWorksmobileSyncer(worksmobileService)
worksmobileHandler := handler.NewWorksmobileHandler(worksmobileService)
apiKeyHandler := handler.NewApiKeyHandler(db)
// 3. Initialize Fiber
@@ -532,6 +585,7 @@ func main() {
// Public Tenant Registration
api.Post("/tenants/registration", tenantHandler.RegisterTenantPublic)
api.Get("/admin/worksmobile/oauth/callback", worksmobileHandler.OAuthCallback)
// Tenant Context Middleware (identifies tenant from Host header)
api.Use(middleware.TenantContextMiddleware(middleware.TenantContextConfig{
@@ -644,6 +698,14 @@ func main() {
admin.Post("/tenants/:id/owners/:userId", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.AddOwner)
admin.Delete("/tenants/:id/owners/:userId", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.RemoveOwner)
admin.Get("/tenants/:tenantId/worksmobile", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.GetOverview)
admin.Get("/tenants/:tenantId/worksmobile/comparison", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.GetComparison)
admin.Get("/tenants/:tenantId/worksmobile/initial-passwords.csv", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.DownloadInitialPasswordsCSV)
admin.Post("/tenants/:tenantId/worksmobile/backfill/dry-run", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.BackfillDryRun)
admin.Post("/tenants/:tenantId/worksmobile/orgunits/:orgUnitId/sync", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.SyncOrgUnit)
admin.Post("/tenants/:tenantId/worksmobile/users/:userId/sync", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.SyncUser)
admin.Post("/tenants/:tenantId/worksmobile/jobs/:jobId/retry", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.RetryJob)
// Organization & Org-Chart Management (Tenant Admin/Super Admin)
org := admin.Group("/tenants/:tenantId/organization")
org.Get("/", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), userGroupHandler.List)

View File

@@ -0,0 +1,39 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestGetEnvFileOrValueReadsSecretFile(t *testing.T) {
t.Setenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY", "inline-value")
secretPath := filepath.Join(t.TempDir(), "worksmobile-private-key.pem")
want := "-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----\n"
if err := os.WriteFile(secretPath, []byte(want), 0o600); err != nil {
t.Fatalf("failed to write secret file: %v", err)
}
t.Setenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE", secretPath)
got, err := getEnvFileOrValue("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE", "WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY", "")
if err != nil {
t.Fatalf("getEnvFileOrValue returned error: %v", err)
}
if got != want {
t.Fatalf("secret value = %q, want file content", got)
}
}
func TestGetEnvFileOrValueFallsBackToRawEnv(t *testing.T) {
t.Setenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY", "inline-value")
t.Setenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE", "")
got, err := getEnvFileOrValue("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE", "WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY", "")
if err != nil {
t.Fatalf("getEnvFileOrValue returned error: %v", err)
}
if got != "inline-value" {
t.Fatalf("secret value = %q, want raw env value", got)
}
}