forked from baron/baron-sso
oathkeeper-introspect 연동 앱 목록 노출 제외
This commit is contained in:
@@ -142,6 +142,10 @@ type clientUpsertRequest struct {
|
||||
Metadata *map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
var protectedSystemClientIDs = map[string]struct{}{
|
||||
"oathkeeper-introspect": {},
|
||||
}
|
||||
|
||||
func normalizeUserRole(role string) string {
|
||||
return domain.NormalizeRole(role)
|
||||
}
|
||||
@@ -263,6 +267,15 @@ func profileRole(profile *domain.UserProfileResponse) string {
|
||||
return strings.TrimSpace(profile.Role)
|
||||
}
|
||||
|
||||
func isProtectedSystemClientID(clientID string) bool {
|
||||
_, ok := protectedSystemClientIDs[strings.TrimSpace(clientID)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func isProtectedSystemClient(client domain.HydraClient) bool {
|
||||
return isProtectedSystemClientID(client.ClientID)
|
||||
}
|
||||
|
||||
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if (!ok || profile == nil) && h.Auth != nil {
|
||||
@@ -557,6 +570,10 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
|
||||
items := make([]clientSummary, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
if isProtectedSystemClient(client) {
|
||||
continue
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(client)
|
||||
|
||||
// 1. [Security] Filter out 'private' clients if user is not an AppManager
|
||||
@@ -604,6 +621,10 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if isProtectedSystemClient(*client) {
|
||||
return errorJSON(c, fiber.StatusNotFound, "client not found")
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*client)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
@@ -678,6 +699,10 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if isProtectedSystemClient(*current) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
@@ -759,6 +784,9 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
if clientID == "" {
|
||||
clientID = uuid.NewString()
|
||||
}
|
||||
if isProtectedSystemClientID(clientID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: reserved system client id")
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(valueOr(req.Name, ""))
|
||||
if name == "" {
|
||||
@@ -899,6 +927,10 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if isProtectedSystemClient(*current) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||
}
|
||||
|
||||
currentSummary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
@@ -1030,6 +1062,10 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if isProtectedSystemClient(*current) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
@@ -1265,6 +1301,10 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if isProtectedSystemClient(*current) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
@@ -1462,6 +1502,9 @@ func (h *DevHandler) GetStats(c *fiber.Ctx) error {
|
||||
var totalClients int64
|
||||
if err == nil {
|
||||
for _, client := range clients {
|
||||
if isProtectedSystemClient(client) {
|
||||
continue
|
||||
}
|
||||
if isSuperAdmin {
|
||||
totalClients++
|
||||
continue
|
||||
|
||||
@@ -124,6 +124,44 @@ func TestListClients_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestListClients_ProtectedSystemClientHidden(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Path == "/clients" {
|
||||
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
|
||||
{"client_id": "oathkeeper-introspect", "client_name": "Internal Client"},
|
||||
{"client_id": "client-1", "client_name": "App One", "metadata": map[string]interface{}{"status": "active"}},
|
||||
}), nil
|
||||
}
|
||||
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||
})
|
||||
|
||||
mockKeto := new(devMockKetoService)
|
||||
mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "AppManager", "member").Return(true, nil)
|
||||
|
||||
h := &DevHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
},
|
||||
Keto: mockKeto,
|
||||
}
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||
return c.Next()
|
||||
})
|
||||
app.Get("/api/v1/dev/clients", h.ListClients)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
|
||||
resp, _ := app.Test(req, -1)
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
var res clientListResponse
|
||||
_ = json.NewDecoder(resp.Body).Decode(&res)
|
||||
assert.Len(t, res.Items, 1)
|
||||
assert.Equal(t, "client-1", res.Items[0].ID)
|
||||
}
|
||||
|
||||
func TestUpdateClientStatus_Success(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
||||
@@ -164,6 +202,38 @@ func TestUpdateClientStatus_Success(t *testing.T) {
|
||||
assert.Equal(t, "inactive", res.Client.Status)
|
||||
}
|
||||
|
||||
func TestUpdateClientStatus_ProtectedSystemClientForbidden(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/oathkeeper-introspect" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"client_id": "oathkeeper-introspect",
|
||||
}), nil
|
||||
}
|
||||
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||
})
|
||||
|
||||
h := &DevHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
},
|
||||
Keto: new(devMockKetoService),
|
||||
}
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
|
||||
return c.Next()
|
||||
})
|
||||
app.Patch("/api/v1/dev/clients/:id/status", h.UpdateClientStatus)
|
||||
|
||||
body, _ := json.Marshal(map[string]interface{}{"status": "inactive"})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/dev/clients/oathkeeper-introspect/status", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req, -1)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestDeleteClient_Success(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
||||
@@ -204,6 +274,67 @@ func TestDeleteClient_Success(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteClient_ProtectedSystemClientForbidden(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/oathkeeper-introspect" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{"client_id": "oathkeeper-introspect"}), nil
|
||||
}
|
||||
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||
})
|
||||
|
||||
h := &DevHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
},
|
||||
SecretRepo: &mockSecretRepo{secrets: map[string]string{"oathkeeper-introspect": "secret"}},
|
||||
Redis: &devMockRedisRepo{data: map[string]string{"client_secret:oathkeeper-introspect": "secret"}},
|
||||
Keto: new(devMockKetoService),
|
||||
}
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
|
||||
return c.Next()
|
||||
})
|
||||
app.Delete("/api/v1/dev/clients/:id", h.DeleteClient)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/dev/clients/oathkeeper-introspect", nil)
|
||||
resp, _ := app.Test(req, -1)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetClient_ProtectedSystemClientHidden(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/oathkeeper-introspect" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"client_id": "oathkeeper-introspect",
|
||||
"client_name": "Internal Client",
|
||||
}), nil
|
||||
}
|
||||
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||
})
|
||||
|
||||
h := &DevHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
},
|
||||
Keto: new(devMockKetoService),
|
||||
}
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
|
||||
return c.Next()
|
||||
})
|
||||
app.Get("/api/v1/dev/clients/:id", h.GetClient)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/oathkeeper-introspect", nil)
|
||||
resp, _ := app.Test(req, -1)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestRotateClientSecret_Success(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
||||
@@ -254,6 +385,7 @@ func TestGetStats_Success(t *testing.T) {
|
||||
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
|
||||
{"client_id": "c1", "metadata": map[string]interface{}{"tenant_id": "t1"}},
|
||||
{"client_id": "c2", "metadata": map[string]interface{}{"tenant_id": "t1"}},
|
||||
{"client_id": "oathkeeper-introspect", "metadata": map[string]interface{}{"tenant_id": "t1"}},
|
||||
{"client_id": "c3", "metadata": map[string]interface{}{"tenant_id": "t2"}},
|
||||
}), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user