1
0
forked from baron/baron-sso

refactor: backend tenant_group 제거 및 리팩터 반영

This commit is contained in:
Lectom C Han
2026-02-12 22:14:34 +09:00
parent b0792113ae
commit a8a219d7ef
26 changed files with 494 additions and 1001 deletions

View File

@@ -2,7 +2,6 @@ package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/utils"
"bytes"
"context"
"encoding/json"
@@ -28,8 +27,8 @@ type HydraAdminService struct {
func NewHydraAdminService() *HydraAdminService {
return &HydraAdminService{
AdminURL: utils.GetEnv("HYDRA_ADMIN_URL", "http://hydra:4445"),
PublicURL: utils.GetEnv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
AdminURL: getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
PublicURL: getenv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
}
}
@@ -47,7 +46,7 @@ func (s *HydraAdminService) ListClients(ctx context.Context, limit, offset int)
return nil, err
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
@@ -75,7 +74,7 @@ func (s *HydraAdminService) GetClient(ctx context.Context, clientID string) (*do
return nil, err
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
@@ -114,7 +113,7 @@ func (s *HydraAdminService) PatchClientStatus(ctx context.Context, clientID, sta
}
req.Header.Set("Content-Type", "application/json-patch+json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
@@ -145,7 +144,7 @@ func (s *HydraAdminService) CreateClient(ctx context.Context, client domain.Hydr
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
@@ -174,7 +173,7 @@ func (s *HydraAdminService) UpdateClient(ctx context.Context, clientID string, c
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
@@ -202,7 +201,7 @@ func (s *HydraAdminService) DeleteClient(ctx context.Context, clientID string) e
return err
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return err
}
@@ -235,7 +234,7 @@ func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, cl
return nil, err
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
@@ -276,7 +275,7 @@ func (s *HydraAdminService) RevokeConsentSessions(ctx context.Context, subject,
return err
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return err
}
@@ -289,7 +288,7 @@ func (s *HydraAdminService) RevokeConsentSessions(ctx context.Context, subject,
return nil
}
func (s *HydraAdminService) HttpClient() *http.Client {
func (s *HydraAdminService) httpClient() *http.Client {
if s.HTTPClient != nil {
return s.HTTPClient
}
@@ -367,7 +366,7 @@ func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge str
return nil, fmt.Errorf("hydra admin: create request for get consent failed: %w", err)
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("hydra admin: get consent request failed: %w", err)
}
@@ -407,7 +406,7 @@ func (s *HydraAdminService) RejectConsentRequest(ctx context.Context, challenge
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("hydra admin: reject consent request failed: %w", err)
}
@@ -449,7 +448,7 @@ func (s *HydraAdminService) RejectLoginRequest(ctx context.Context, challenge, e
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("hydra admin: reject login request failed: %w", err)
}
@@ -484,7 +483,7 @@ func (s *HydraAdminService) GetLoginRequest(ctx context.Context, challenge strin
return nil, fmt.Errorf("hydra admin: create request for get login failed: %w", err)
}
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("hydra admin: get login request failed: %w", err)
}
@@ -532,7 +531,7 @@ func (s *HydraAdminService) AcceptConsentRequest(ctx context.Context, challenge
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("hydra admin: accept consent request failed: %w", err)
}
@@ -576,7 +575,7 @@ func (s *HydraAdminService) AcceptLoginRequest(ctx context.Context, challenge st
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.HttpClient().Do(req)
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("hydra admin: accept login request failed: %w", err)
}
@@ -597,34 +596,3 @@ func (s *HydraAdminService) AcceptLoginRequest(ctx context.Context, challenge st
return &AcceptLoginRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
}
func (s *HydraAdminService) IntrospectToken(ctx context.Context, token string) (map[string]interface{}, error) {
endpoint := fmt.Sprintf("%s/admin/oauth2/introspect", strings.TrimRight(s.AdminURL, "/"))
data := url.Values{}
data.Set("token", token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.HttpClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return nil, fmt.Errorf("hydra admin: introspect failed status=%d body=%s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -1,7 +1,6 @@
package service
import (
"baron-sso-backend/internal/utils"
"bytes"
"context"
"encoding/json"
@@ -10,6 +9,7 @@ import (
"log/slog"
"net/http"
"net/url"
"os"
"time"
)
@@ -18,7 +18,6 @@ type KetoService interface {
CreateRelation(ctx context.Context, namespace, object, relation, subject string) error
DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error
ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]RelationTuple, error)
ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error)
}
type ketoService struct {
@@ -28,8 +27,14 @@ type ketoService struct {
}
func NewKetoService() KetoService {
readURL := utils.GetEnv("KETO_READ_URL", "http://keto:4466")
writeURL := utils.GetEnv("KETO_WRITE_URL", "http://keto:4467")
readURL := os.Getenv("KETO_READ_URL")
if readURL == "" {
readURL = "http://keto:4466"
}
writeURL := os.Getenv("KETO_WRITE_URL")
if writeURL == "" {
writeURL = "http://keto:4467"
}
return &ketoService{
readURL: readURL,
@@ -187,40 +192,3 @@ func (s *ketoService) DeleteRelation(ctx context.Context, namespace, object, rel
slog.Info("Keto relation deleted", "namespace", namespace, "object", object, "relation", relation, "subject", subject)
return nil
}
func (s *ketoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
u, _ := url.Parse(fmt.Sprintf("%s/relation-tuples", s.readURL))
q := u.Query()
q.Set("namespace", namespace)
q.Set("relation", relation)
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 nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("keto returned status %d: %s", resp.StatusCode, string(body))
}
var res relationTuplesResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
objects := make([]string, 0, len(res.RelationTuples))
seen := make(map[string]bool)
for _, rt := range res.RelationTuples {
if !seen[rt.Object] {
objects = append(objects, rt.Object)
seen[rt.Object] = true
}
}
return objects, nil
}

View File

@@ -1,7 +1,6 @@
package service
import (
"baron-sso-backend/internal/utils"
"bytes"
"context"
"encoding/json"
@@ -29,7 +28,7 @@ type KratosAdminService struct {
func NewKratosAdminService() *KratosAdminService {
return &KratosAdminService{
AdminURL: utils.GetEnv("KRATOS_ADMIN_URL", "http://kratos:4434"),
AdminURL: getenvKratos("KRATOS_ADMIN_URL", "http://kratos:4434"),
}
}
@@ -228,9 +227,8 @@ func (s *KratosAdminService) httpClient() *http.Client {
}
func getenvKratos(key, fallback string) string {
v := os.Getenv(key)
if v == "" {
return fallback
if v := os.Getenv(key); v != "" {
return v
}
return strings.Trim(v, "\"")
return fallback
}

View File

@@ -2,7 +2,6 @@ package service
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/utils"
"bytes"
"context"
"encoding/json"
@@ -28,9 +27,9 @@ type OryProvider struct {
func NewOryProvider() *OryProvider {
return &OryProvider{
KratosAdminURL: utils.GetEnv("KRATOS_ADMIN_URL", "http://kratos:4434"),
KratosPublicURL: utils.GetEnv("KRATOS_PUBLIC_URL", "http://kratos:4433"),
HydraAdminURL: utils.GetEnv("HYDRA_ADMIN_URL", "http://hydra:4445"),
KratosAdminURL: getenv("KRATOS_ADMIN_URL", "http://kratos:4434"),
KratosPublicURL: getenv("KRATOS_PUBLIC_URL", "http://kratos:4433"),
HydraAdminURL: getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
}
}
@@ -320,7 +319,6 @@ func (o *OryProvider) submitLoginCodeInit(loginID, returnTo string) (*domain.Lin
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
if resp.StatusCode >= 300 {
slog.Warn("Ory link login init failed", "loginID", loginID, "flow_id", flowID, "status", resp.StatusCode, "body", string(respBody))
init, ok := parseKratosLinkLoginResponse(flowID, respBody)
if ok {
slog.Info("Ory link login initiated with non-2xx response", "loginID", loginID, "flow_id", flowID, "status", resp.StatusCode)
@@ -729,12 +727,10 @@ func (o *OryProvider) UpdateUserPassword(loginID, newPassword string, r *http.Re
}
func getenv(key, fallback string) string {
v := os.Getenv(key)
if v == "" {
return fallback
if v := os.Getenv(key); v != "" {
return v
}
// Strip surrounding double quotes if present
return strings.Trim(v, "\"")
return fallback
}
// findIdentityID: Kratos Admin API에서 credentials_identifier로 검색 후 첫 번째 identity id 반환

View File

@@ -15,10 +15,6 @@ type RelyingPartyService interface {
ListByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.RelyingParty, error)
Update(ctx context.Context, clientID string, client domain.HydraClient) (*domain.RelyingParty, error)
Delete(ctx context.Context, clientID string) error
CheckPermission(ctx context.Context, userID, clientID, relation string) (bool, error)
AddOwner(ctx context.Context, clientID, subject string) error
RemoveOwner(ctx context.Context, clientID, subject string) error
ListOwners(ctx context.Context, clientID string) ([]string, error)
}
type relyingPartyService struct {
@@ -162,31 +158,6 @@ func (s *relyingPartyService) Delete(ctx context.Context, clientID string) error
return nil
}
func (s *relyingPartyService) CheckPermission(ctx context.Context, userID, clientID, relation string) (bool, error) {
return s.ketoService.CheckPermission(ctx, userID, "RelyingParty", clientID, relation)
}
func (s *relyingPartyService) AddOwner(ctx context.Context, clientID, subject string) error {
return s.ketoService.CreateRelation(ctx, "RelyingParty", clientID, "owners", subject)
}
func (s *relyingPartyService) RemoveOwner(ctx context.Context, clientID, subject string) error {
return s.ketoService.DeleteRelation(ctx, "RelyingParty", clientID, "owners", subject)
}
func (s *relyingPartyService) ListOwners(ctx context.Context, clientID string) ([]string, error) {
tuples, err := s.ketoService.ListRelations(ctx, "RelyingParty", clientID, "owners", "")
if err != nil {
return nil, err
}
subjects := make([]string, 0, len(tuples))
for _, t := range tuples {
subjects = append(subjects, t.SubjectID)
}
return subjects, nil
}
func (s *relyingPartyService) mapHydraToDomain(client *domain.HydraClient) *domain.RelyingParty {
if client == nil {
return nil

View File

@@ -54,14 +54,6 @@ func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object,
return args.Get(0).([]RelationTuple), args.Error(1)
}
func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
args := m.Called(ctx, namespace, relation, subject)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]string), args.Error(1)
}
// --- Test Helpers ---
type hydraRoundTripperFunc func(*http.Request) (*http.Response, error)

View File

@@ -18,12 +18,8 @@ type TenantService interface {
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
ApproveTenant(ctx context.Context, id string) error
SetKetoService(keto KetoService) // 추가
AddTenantAdmin(ctx context.Context, tenantID, userID string) error
RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error
ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error)
}
type tenantService struct {
@@ -43,60 +39,6 @@ func (s *tenantService) GetTenant(ctx context.Context, id string) (*domain.Tenan
return s.repo.FindByID(ctx, id)
}
func (s *tenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
if s.keto == nil {
return nil, errors.New("keto service not initialized")
}
// 1. Get directly managed tenants
directTenantIDs, err := s.keto.ListObjects(ctx, "Tenant", "admins", userID)
if err != nil {
slog.Error("Failed to list directly managed tenants from Keto", "userID", userID, "error", err)
}
// 2. Get managed tenant groups
groupIDs, err := s.keto.ListObjects(ctx, "TenantGroup", "admins", userID)
if err != nil {
slog.Error("Failed to list managed tenant groups from Keto", "userID", userID, "error", err)
}
// 3. Get tenants belonging to those groups
var groupInheritedTenantIDs []string
for _, groupID := range groupIDs {
// In Keto, we defined: Tenant#parent_group@TenantGroup:GroupID#_
// To find tenants in a group, we look for relations where namespace=Tenant, relation=parent_group, subject=TenantGroup:GroupID#_
// Wait, my ListObjects lists objects given a subject.
// So subject="TenantGroup:"+groupID+"#_"
// Object is Tenant ID.
ts, err := s.keto.ListRelations(ctx, "Tenant", "", "parent_group", "TenantGroup:"+groupID)
if err == nil {
for _, t := range ts {
groupInheritedTenantIDs = append(groupInheritedTenantIDs, t.Object)
}
}
}
// Combine and deduplicate IDs
allIDsMap := make(map[string]bool)
for _, id := range directTenantIDs {
allIDsMap[id] = true
}
for _, id := range groupInheritedTenantIDs {
allIDsMap[id] = true
}
allIDs := make([]string, 0, len(allIDsMap))
for id := range allIDsMap {
allIDs = append(allIDs, id)
}
if len(allIDs) == 0 {
return []domain.Tenant{}, nil
}
return s.repo.FindByIDs(ctx, allIDs)
}
func (s *tenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error) {
// Validate Slug
if ok, msg := utils.ValidateSlug(slug); !ok {
@@ -211,35 +153,3 @@ func (s *tenantService) GetTenantByDomain(ctx context.Context, emailDomain strin
func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
return s.repo.FindBySlug(ctx, slug)
}
func (s *tenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
if s.keto == nil {
return errors.New("keto service not initialized")
}
return s.keto.CreateRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
}
func (s *tenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
if s.keto == nil {
return errors.New("keto service not initialized")
}
return s.keto.DeleteRelation(ctx, "Tenant", tenantID, "admins", "User:"+userID)
}
func (s *tenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
if s.keto == nil {
return nil, errors.New("keto service not initialized")
}
tuples, err := s.keto.ListRelations(ctx, "Tenant", tenantID, "admins", "")
if err != nil {
return nil, err
}
userIDs := make([]string, 0, len(tuples))
for _, t := range tuples {
if len(t.SubjectID) > 5 && t.SubjectID[:5] == "User:" {
userIDs = append(userIDs, t.SubjectID[5:])
}
}
return userIDs, nil
}