forked from baron/baron-sso
Merge origin/dev into dev
This commit is contained in:
@@ -84,20 +84,36 @@ func (h *TenantHandler) SetWorksmobileSyncer(syncer service.WorksmobileSyncer) {
|
||||
h.Worksmobile = syncer
|
||||
}
|
||||
|
||||
type tenantPermissions struct {
|
||||
View bool `json:"view"`
|
||||
Manage bool `json:"manage"`
|
||||
ManageAdmins bool `json:"manage_admins"`
|
||||
|
||||
ViewProfile bool `json:"view_profile"`
|
||||
ManageProfile bool `json:"manage_profile"`
|
||||
ViewPermissions bool `json:"view_permissions"`
|
||||
ManagePermissions bool `json:"manage_permissions"`
|
||||
ViewOrganization bool `json:"view_organization"`
|
||||
ManageOrganization bool `json:"manage_organization"`
|
||||
ViewSchema bool `json:"view_schema"`
|
||||
ManageSchema bool `json:"manage_schema"`
|
||||
}
|
||||
|
||||
type tenantSummary struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
ParentID *string `json:"parentId"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Config domain.JSONMap `json:"config,omitempty"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
TotalMemberCount int64 `json:"totalMemberCount"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
ParentID *string `json:"parentId"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Config domain.JSONMap `json:"config,omitempty"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
TotalMemberCount int64 `json:"totalMemberCount"`
|
||||
UserPermissions *tenantPermissions `json:"userPermissions,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type tenantListResponse struct {
|
||||
@@ -1709,6 +1725,83 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
||||
summary.MemberCount = memberCounts[tenant.ID]
|
||||
summary.TotalMemberCount = totalMemberCounts[tenant.ID]
|
||||
|
||||
// Populate Keto-based permissions for the current user
|
||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if ok && profile != nil {
|
||||
role := domain.NormalizeRole(profile.Role)
|
||||
if role == domain.RoleSuperAdmin {
|
||||
summary.UserPermissions = &tenantPermissions{
|
||||
View: true,
|
||||
Manage: true,
|
||||
ManageAdmins: true,
|
||||
ViewProfile: true,
|
||||
ManageProfile: true,
|
||||
ViewPermissions: true,
|
||||
ManagePermissions: true,
|
||||
ViewOrganization: true,
|
||||
ManageOrganization: true,
|
||||
ViewSchema: true,
|
||||
ManageSchema: true,
|
||||
}
|
||||
} else {
|
||||
// Query Keto in parallel for maximum performance
|
||||
subject := "User:" + profile.ID
|
||||
type checkResult struct {
|
||||
relation string
|
||||
allowed bool
|
||||
err error
|
||||
}
|
||||
ch := make(chan checkResult, 11)
|
||||
relations := []string{
|
||||
"view", "manage", "manage_admins",
|
||||
"view_profile", "manage_profile",
|
||||
"view_permissions", "manage_permissions",
|
||||
"view_organization", "manage_organization",
|
||||
"view_schema", "manage_schema",
|
||||
}
|
||||
for _, rel := range relations {
|
||||
go func(r string) {
|
||||
allowed, err := h.Keto.CheckPermission(c.Context(), subject, "Tenant", tenant.ID, r)
|
||||
ch <- checkResult{relation: r, allowed: allowed, err: err}
|
||||
}(rel)
|
||||
}
|
||||
|
||||
perms := &tenantPermissions{}
|
||||
for range relations {
|
||||
res := <-ch
|
||||
if res.err != nil {
|
||||
slog.Error("Failed to check Keto permission in GetTenant", "error", res.err, "relation", res.relation, "userID", profile.ID, "tenantID", tenant.ID)
|
||||
continue
|
||||
}
|
||||
switch res.relation {
|
||||
case "view":
|
||||
perms.View = res.allowed
|
||||
case "manage":
|
||||
perms.Manage = res.allowed
|
||||
case "manage_admins":
|
||||
perms.ManageAdmins = res.allowed
|
||||
case "view_profile":
|
||||
perms.ViewProfile = res.allowed
|
||||
case "manage_profile":
|
||||
perms.ManageProfile = res.allowed
|
||||
case "view_permissions":
|
||||
perms.ViewPermissions = res.allowed
|
||||
case "manage_permissions":
|
||||
perms.ManagePermissions = res.allowed
|
||||
case "view_organization":
|
||||
perms.ViewOrganization = res.allowed
|
||||
case "manage_organization":
|
||||
perms.ManageOrganization = res.allowed
|
||||
case "view_schema":
|
||||
perms.ViewSchema = res.allowed
|
||||
case "manage_schema":
|
||||
perms.ManageSchema = res.allowed
|
||||
}
|
||||
}
|
||||
summary.UserPermissions = perms
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(summary)
|
||||
}
|
||||
|
||||
@@ -3652,3 +3745,419 @@ func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
|
||||
"sharedWith": link.Name,
|
||||
})
|
||||
}
|
||||
|
||||
type tenantRelationRequest struct {
|
||||
UserID string `json:"userId"`
|
||||
Relation string `json:"relation"`
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ListRelations(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
if tenantID == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
||||
}
|
||||
|
||||
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
allowedRelations := map[string]bool{
|
||||
"profile_viewers": true,
|
||||
"profile_managers": true,
|
||||
"permissions_viewers": true,
|
||||
"permissions_managers": true,
|
||||
"organization_viewers": true,
|
||||
"organization_managers": true,
|
||||
"schema_viewers": true,
|
||||
"schema_managers": true,
|
||||
}
|
||||
|
||||
type userRelationInfo struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Relations []string `json:"relations"`
|
||||
}
|
||||
|
||||
userMap := make(map[string][]string)
|
||||
for _, rel := range relations {
|
||||
if !allowedRelations[rel.Relation] {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(rel.SubjectID, "User:") {
|
||||
continue
|
||||
}
|
||||
userID := strings.TrimPrefix(rel.SubjectID, "User:")
|
||||
userMap[userID] = append(userMap[userID], rel.Relation)
|
||||
}
|
||||
|
||||
items := []userRelationInfo{}
|
||||
for userID, rels := range userMap {
|
||||
name := "Unknown"
|
||||
email := "Unknown"
|
||||
|
||||
if h.KratosAdmin != nil {
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err == nil && identity != nil {
|
||||
if n, ok := identity.Traits["name"].(string); ok {
|
||||
name = n
|
||||
}
|
||||
if e, ok := identity.Traits["email"].(string); ok {
|
||||
email = e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name == "Unknown" && email == "Unknown" && h.UserRepo != nil {
|
||||
user, err := h.UserRepo.FindByID(c.Context(), userID)
|
||||
if err == nil && user != nil {
|
||||
name = user.Name
|
||||
email = user.Email
|
||||
} else if userID == "00000000-0000-0000-0000-000000000000" {
|
||||
name = "Dev Mock User"
|
||||
email = "mock@hmac.kr"
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, userRelationInfo{
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Relations: rels,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) AddRelation(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
if tenantID == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
||||
}
|
||||
|
||||
var req tenantRelationRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
if req.UserID == "" || req.Relation == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
||||
}
|
||||
|
||||
allowedRelations := map[string]bool{
|
||||
"profile_viewers": true,
|
||||
"profile_managers": true,
|
||||
"permissions_viewers": true,
|
||||
"permissions_managers": true,
|
||||
"organization_viewers": true,
|
||||
"organization_managers": true,
|
||||
"schema_viewers": true,
|
||||
"schema_managers": true,
|
||||
}
|
||||
|
||||
if !allowedRelations[req.Relation] {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid or unsupported relation")
|
||||
}
|
||||
|
||||
if h.Keto != nil {
|
||||
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, req.Relation, "User:"+req.UserID)
|
||||
if err == nil && len(relations) > 0 {
|
||||
return errorJSON(c, fiber.StatusConflict, "이미 해당 세부 권한이 등록된 사용자입니다.")
|
||||
}
|
||||
}
|
||||
|
||||
var directWriteErr error
|
||||
if h.Keto != nil {
|
||||
directWriteErr = h.Keto.CreateRelation(c.Context(), "Tenant", tenantID, req.Relation, "User:"+req.UserID)
|
||||
}
|
||||
|
||||
if h.KetoOutbox != nil {
|
||||
status := domain.KetoOutboxStatusPending
|
||||
var processedAt *time.Time
|
||||
if directWriteErr == nil && h.Keto != nil {
|
||||
status = domain.KetoOutboxStatusProcessed
|
||||
now := time.Now()
|
||||
processedAt = &now
|
||||
}
|
||||
|
||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenantID,
|
||||
Relation: req.Relation,
|
||||
Subject: "User:" + req.UserID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
Status: status,
|
||||
ProcessedAt: processedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if directWriteErr != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) RemoveRelation(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
if tenantID == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
|
||||
}
|
||||
|
||||
var req tenantRelationRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
if req.UserID == "" || req.Relation == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
||||
}
|
||||
|
||||
var directWriteErr error
|
||||
if h.Keto != nil {
|
||||
directWriteErr = h.Keto.DeleteRelation(c.Context(), "Tenant", tenantID, req.Relation, "User:"+req.UserID)
|
||||
}
|
||||
|
||||
if h.KetoOutbox != nil {
|
||||
status := domain.KetoOutboxStatusPending
|
||||
var processedAt *time.Time
|
||||
if directWriteErr == nil && h.Keto != nil {
|
||||
status = domain.KetoOutboxStatusProcessed
|
||||
now := time.Now()
|
||||
processedAt = &now
|
||||
}
|
||||
|
||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenantID,
|
||||
Relation: req.Relation,
|
||||
Subject: "User:" + req.UserID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
Status: status,
|
||||
ProcessedAt: processedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if directWriteErr != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ListSystemRelations(c *fiber.Ctx) error {
|
||||
relations, err := h.Keto.ListRelations(c.Context(), "System", "system", "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
allowedRelations := map[string]bool{
|
||||
"overview_viewers": true,
|
||||
"tenants_viewers": true,
|
||||
"org_chart_viewers": true,
|
||||
"worksmobile_viewers": true,
|
||||
"ory_ssot_viewers": true,
|
||||
"data_integrity_viewers": true,
|
||||
"users_viewers": true,
|
||||
"permissions_direct_viewers": true,
|
||||
"auth_guard_viewers": true,
|
||||
"api_keys_viewers": true,
|
||||
"audit_logs_viewers": true,
|
||||
|
||||
"overview_managers": true,
|
||||
"tenants_managers": true,
|
||||
"org_chart_managers": true,
|
||||
"worksmobile_managers": true,
|
||||
"ory_ssot_managers": true,
|
||||
"data_integrity_managers": true,
|
||||
"users_managers": true,
|
||||
"permissions_direct_managers": true,
|
||||
"auth_guard_managers": true,
|
||||
"api_keys_managers": true,
|
||||
"audit_logs_managers": true,
|
||||
}
|
||||
|
||||
type userRelationInfo struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Relations []string `json:"relations"`
|
||||
}
|
||||
|
||||
userMap := make(map[string][]string)
|
||||
for _, rel := range relations {
|
||||
if !allowedRelations[rel.Relation] {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(rel.SubjectID, "User:") {
|
||||
continue
|
||||
}
|
||||
userID := strings.TrimPrefix(rel.SubjectID, "User:")
|
||||
userMap[userID] = append(userMap[userID], rel.Relation)
|
||||
}
|
||||
|
||||
items := []userRelationInfo{}
|
||||
for userID, rels := range userMap {
|
||||
name := "Unknown"
|
||||
email := "Unknown"
|
||||
|
||||
if h.KratosAdmin != nil {
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err == nil && identity != nil {
|
||||
if n, ok := identity.Traits["name"].(string); ok {
|
||||
name = n
|
||||
}
|
||||
if e, ok := identity.Traits["email"].(string); ok {
|
||||
email = e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name == "Unknown" && email == "Unknown" && h.UserRepo != nil {
|
||||
user, err := h.UserRepo.FindByID(c.Context(), userID)
|
||||
if err == nil && user != nil {
|
||||
name = user.Name
|
||||
email = user.Email
|
||||
} else if userID == "00000000-0000-0000-0000-000000000000" {
|
||||
name = "Dev Mock User"
|
||||
email = "mock@hmac.kr"
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, userRelationInfo{
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Relations: rels,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) AddSystemRelation(c *fiber.Ctx) error {
|
||||
var req tenantRelationRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
if req.UserID == "" || req.Relation == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
||||
}
|
||||
|
||||
allowedRelations := map[string]bool{
|
||||
"overview_viewers": true,
|
||||
"tenants_viewers": true,
|
||||
"org_chart_viewers": true,
|
||||
"worksmobile_viewers": true,
|
||||
"ory_ssot_viewers": true,
|
||||
"data_integrity_viewers": true,
|
||||
"users_viewers": true,
|
||||
"permissions_direct_viewers": true,
|
||||
"auth_guard_viewers": true,
|
||||
"api_keys_viewers": true,
|
||||
"audit_logs_viewers": true,
|
||||
|
||||
"overview_managers": true,
|
||||
"tenants_managers": true,
|
||||
"org_chart_managers": true,
|
||||
"worksmobile_managers": true,
|
||||
"ory_ssot_managers": true,
|
||||
"data_integrity_managers": true,
|
||||
"users_managers": true,
|
||||
"permissions_direct_managers": true,
|
||||
"auth_guard_managers": true,
|
||||
"api_keys_managers": true,
|
||||
"audit_logs_managers": true,
|
||||
}
|
||||
|
||||
if !allowedRelations[req.Relation] {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid or unsupported relation")
|
||||
}
|
||||
|
||||
if h.Keto != nil {
|
||||
relations, err := h.Keto.ListRelations(c.Context(), "System", "system", req.Relation, "User:"+req.UserID)
|
||||
if err == nil && len(relations) > 0 {
|
||||
return errorJSON(c, fiber.StatusConflict, "이미 해당 세부 권한이 등록된 사용자입니다.")
|
||||
}
|
||||
}
|
||||
|
||||
var directWriteErr error
|
||||
if h.Keto != nil {
|
||||
directWriteErr = h.Keto.CreateRelation(c.Context(), "System", "system", req.Relation, "User:"+req.UserID)
|
||||
}
|
||||
|
||||
if h.KetoOutbox != nil {
|
||||
status := domain.KetoOutboxStatusPending
|
||||
var processedAt *time.Time
|
||||
if directWriteErr == nil && h.Keto != nil {
|
||||
status = domain.KetoOutboxStatusProcessed
|
||||
now := time.Now()
|
||||
processedAt = &now
|
||||
}
|
||||
|
||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
Object: "system",
|
||||
Relation: req.Relation,
|
||||
Subject: "User:" + req.UserID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
Status: status,
|
||||
ProcessedAt: processedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if directWriteErr != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) RemoveSystemRelation(c *fiber.Ctx) error {
|
||||
var req tenantRelationRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
if req.UserID == "" || req.Relation == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
||||
}
|
||||
|
||||
var directWriteErr error
|
||||
if h.Keto != nil {
|
||||
directWriteErr = h.Keto.DeleteRelation(c.Context(), "System", "system", req.Relation, "User:"+req.UserID)
|
||||
}
|
||||
|
||||
if h.KetoOutbox != nil {
|
||||
status := domain.KetoOutboxStatusPending
|
||||
var processedAt *time.Time
|
||||
if directWriteErr == nil && h.Keto != nil {
|
||||
status = domain.KetoOutboxStatusProcessed
|
||||
now := time.Now()
|
||||
processedAt = &now
|
||||
}
|
||||
|
||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
Object: "system",
|
||||
Relation: req.Relation,
|
||||
Subject: "User:" + req.UserID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
Status: status,
|
||||
ProcessedAt: processedAt,
|
||||
})
|
||||
}
|
||||
|
||||
if directWriteErr != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user