forked from baron/baron-sso
Dev API에 RP operator relation 조회/부여/회수 추가
This commit is contained in:
@@ -31,6 +31,7 @@ type DevHandler struct {
|
||||
KratosAdmin service.KratosAdminService
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
Keto service.KetoService
|
||||
KetoOutbox repository.KetoOutboxRepository
|
||||
RPSvc service.RelyingPartyService
|
||||
TenantSvc service.TenantService
|
||||
Auth interface {
|
||||
@@ -43,7 +44,9 @@ func NewDevHandler(
|
||||
secretRepo domain.ClientSecretRepository,
|
||||
consentRepo repository.ClientConsentRepository,
|
||||
rpSvc service.RelyingPartyService,
|
||||
keto service.KetoService, tenantSvc service.TenantService,
|
||||
keto service.KetoService,
|
||||
ketoOutbox repository.KetoOutboxRepository,
|
||||
tenantSvc service.TenantService,
|
||||
auth ...interface {
|
||||
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||
},
|
||||
@@ -64,6 +67,7 @@ func NewDevHandler(
|
||||
KratosAdmin: service.NewKratosAdminService(),
|
||||
ConsentRepo: consentRepo,
|
||||
Keto: keto,
|
||||
KetoOutbox: ketoOutbox,
|
||||
RPSvc: rpSvc,
|
||||
TenantSvc: tenantSvc,
|
||||
Auth: authProvider,
|
||||
@@ -118,6 +122,23 @@ type clientEndpoints struct {
|
||||
UserInfo string `json:"userinfo"`
|
||||
}
|
||||
|
||||
type clientRelationSummary struct {
|
||||
Relation string `json:"relation"`
|
||||
Subject string `json:"subject"`
|
||||
SubjectType string `json:"subjectType"`
|
||||
SubjectID string `json:"subjectId"`
|
||||
}
|
||||
|
||||
type clientRelationListResponse struct {
|
||||
Items []clientRelationSummary `json:"items"`
|
||||
}
|
||||
|
||||
type clientRelationUpsertRequest struct {
|
||||
Relation string `json:"relation"`
|
||||
Subject string `json:"subject"`
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
|
||||
type consentSummary struct {
|
||||
Subject string `json:"subject"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
@@ -160,6 +181,19 @@ var reservedSystemClientNames = map[string]string{
|
||||
"devfront": "devfront",
|
||||
}
|
||||
|
||||
var allowedRelyingPartyOperatorRelations = map[string]struct{}{
|
||||
"admins": {},
|
||||
"creator": {},
|
||||
"config_editor": {},
|
||||
"secret_rotator": {},
|
||||
"jwks_viewer": {},
|
||||
"jwks_operator": {},
|
||||
"consent_viewer": {},
|
||||
"consent_revoker": {},
|
||||
"relationship_viewer": {},
|
||||
"status_operator": {},
|
||||
}
|
||||
|
||||
func normalizeUserRole(role string) string {
|
||||
return domain.NormalizeRole(role)
|
||||
}
|
||||
@@ -314,6 +348,32 @@ func (h *DevHandler) canOperateClientByPermit(c *fiber.Ctx, profile *domain.User
|
||||
return err == nil && allowed
|
||||
}
|
||||
|
||||
func (h *DevHandler) canViewClientRelations(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) bool {
|
||||
if h.canOperateClientByPermit(c, profile, summary, "view_relationships") {
|
||||
return true
|
||||
}
|
||||
return canAccessClientByLegacyScope(profile, summary)
|
||||
}
|
||||
|
||||
func (h *DevHandler) canManageClientRelations(c *fiber.Ctx, profile *domain.UserProfileResponse, summary clientSummary) bool {
|
||||
if profile == nil {
|
||||
return false
|
||||
}
|
||||
if normalizeUserRole(profile.Role) == domain.RoleSuperAdmin {
|
||||
return true
|
||||
}
|
||||
if h.canOperateClientByPermit(c, profile, summary, "manage") {
|
||||
return true
|
||||
}
|
||||
|
||||
clientTenantID := resolveClientTenantID(summary)
|
||||
if clientTenantID != "" && h.canManageTenantClientsByPermit(c, profile, clientTenantID) {
|
||||
return true
|
||||
}
|
||||
|
||||
return canAccessClientByLegacyScope(profile, summary)
|
||||
}
|
||||
|
||||
func canAccessClientByLegacyScope(profile *domain.UserProfileResponse, summary clientSummary) bool {
|
||||
if profile == nil {
|
||||
return false
|
||||
@@ -389,6 +449,79 @@ func reservedSystemClientOwnerID(name string) (string, bool) {
|
||||
return ownerID, ok
|
||||
}
|
||||
|
||||
func normalizeRelyingPartyRelation(relation string) string {
|
||||
return strings.TrimSpace(relation)
|
||||
}
|
||||
|
||||
func isAllowedRelyingPartyOperatorRelation(relation string) bool {
|
||||
_, ok := allowedRelyingPartyOperatorRelations[normalizeRelyingPartyRelation(relation)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func normalizeClientRelationSubject(subject, userID string) string {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject != "" {
|
||||
return subject
|
||||
}
|
||||
userID = strings.TrimSpace(userID)
|
||||
if userID == "" {
|
||||
return ""
|
||||
}
|
||||
return "User:" + userID
|
||||
}
|
||||
|
||||
func parseClientRelationSubject(subject string) (string, string) {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
return "", ""
|
||||
}
|
||||
parts := strings.SplitN(subject, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
func validateClientRelationWriteInput(relation, subject string) error {
|
||||
relation = normalizeRelyingPartyRelation(relation)
|
||||
if !isAllowedRelyingPartyOperatorRelation(relation) {
|
||||
return fmt.Errorf("unsupported relation")
|
||||
}
|
||||
|
||||
subjectType, subjectID := parseClientRelationSubject(subject)
|
||||
if subjectType != "User" || subjectID == "" || strings.Contains(subjectID, "#") {
|
||||
return fmt.Errorf("subject must be in User:<id> format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapRelationTupleSummary(tuple service.RelationTuple) clientRelationSummary {
|
||||
subjectType, subjectID := parseClientRelationSubject(tuple.SubjectID)
|
||||
return clientRelationSummary{
|
||||
Relation: tuple.Relation,
|
||||
Subject: tuple.SubjectID,
|
||||
SubjectType: subjectType,
|
||||
SubjectID: subjectID,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DevHandler) loadClientSummary(ctx context.Context, clientID string) (clientSummary, error) {
|
||||
clientID = strings.TrimSpace(clientID)
|
||||
if clientID == "" {
|
||||
return clientSummary{}, fmt.Errorf("client id is required")
|
||||
}
|
||||
client, err := h.Hydra.GetClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return clientSummary{}, err
|
||||
}
|
||||
return h.mapClientSummary(*client), nil
|
||||
}
|
||||
|
||||
func (h *DevHandler) getRelationRequestProfile(c *fiber.Ctx) *domain.UserProfileResponse {
|
||||
return h.getCurrentProfile(c)
|
||||
}
|
||||
|
||||
func validateReservedSystemClientName(clientID, name string) error {
|
||||
ownerID, reserved := reservedSystemClientOwnerID(name)
|
||||
if !reserved {
|
||||
@@ -733,6 +866,130 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DevHandler) ListClientRelations(c *fiber.Ctx) error {
|
||||
clientID := strings.TrimSpace(c.Params("id"))
|
||||
if clientID == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
|
||||
}
|
||||
|
||||
profile := h.getRelationRequestProfile(c)
|
||||
summary, err := h.loadClientSummary(c.Context(), clientID)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusNotFound, "client not found")
|
||||
}
|
||||
|
||||
if !h.canViewClientRelations(c, profile, summary) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
if h.Keto == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "keto service unavailable")
|
||||
}
|
||||
|
||||
items := make([]clientRelationSummary, 0)
|
||||
for relation := range allowedRelyingPartyOperatorRelations {
|
||||
tuples, err := h.Keto.ListRelations(c.Context(), "RelyingParty", clientID, relation, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
for _, tuple := range tuples {
|
||||
items = append(items, mapRelationTupleSummary(tuple))
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(clientRelationListResponse{Items: items})
|
||||
}
|
||||
|
||||
func (h *DevHandler) AddClientRelation(c *fiber.Ctx) error {
|
||||
clientID := strings.TrimSpace(c.Params("id"))
|
||||
if clientID == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
|
||||
}
|
||||
|
||||
var req clientRelationUpsertRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
req.Relation = normalizeRelyingPartyRelation(req.Relation)
|
||||
req.Subject = normalizeClientRelationSubject(req.Subject, req.UserID)
|
||||
if err := validateClientRelationWriteInput(req.Relation, req.Subject); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
profile := h.getRelationRequestProfile(c)
|
||||
summary, err := h.loadClientSummary(c.Context(), clientID)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusNotFound, "client not found")
|
||||
}
|
||||
if !h.canManageClientRelations(c, profile, summary) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
if h.Keto == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "keto service unavailable")
|
||||
}
|
||||
if h.KetoOutbox == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "keto outbox unavailable")
|
||||
}
|
||||
|
||||
existing, err := h.Keto.ListRelations(c.Context(), "RelyingParty", clientID, req.Relation, req.Subject)
|
||||
if err == nil && len(existing) > 0 {
|
||||
return errorJSON(c, fiber.StatusConflict, "relation already exists")
|
||||
}
|
||||
|
||||
if err := h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "RelyingParty",
|
||||
Object: clientID,
|
||||
Relation: req.Relation,
|
||||
Subject: req.Subject,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
}); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(mapRelationTupleSummary(service.RelationTuple{
|
||||
Object: clientID,
|
||||
Relation: req.Relation,
|
||||
SubjectID: req.Subject,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *DevHandler) RemoveClientRelation(c *fiber.Ctx) error {
|
||||
clientID := strings.TrimSpace(c.Params("id"))
|
||||
if clientID == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
|
||||
}
|
||||
|
||||
relation := normalizeRelyingPartyRelation(c.Query("relation"))
|
||||
subject := normalizeClientRelationSubject(c.Query("subject"), c.Query("userId"))
|
||||
if err := validateClientRelationWriteInput(relation, subject); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
profile := h.getRelationRequestProfile(c)
|
||||
summary, err := h.loadClientSummary(c.Context(), clientID)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusNotFound, "client not found")
|
||||
}
|
||||
if !h.canManageClientRelations(c, profile, summary) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
if h.KetoOutbox == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "keto outbox unavailable")
|
||||
}
|
||||
|
||||
if err := h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "RelyingParty",
|
||||
Object: clientID,
|
||||
Relation: relation,
|
||||
Subject: subject,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
}); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
||||
h.injectTenantContextFromHeader(c)
|
||||
clientID := c.Params("id")
|
||||
|
||||
Reference in New Issue
Block a user