forked from baron/baron-sso
Merge branch 'dev' into feature/af-ui
This commit is contained in:
@@ -146,6 +146,11 @@ var protectedSystemClientIDs = map[string]struct{}{
|
|||||||
"oathkeeper-introspect": {},
|
"oathkeeper-introspect": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reservedSystemClientNames = map[string]string{
|
||||||
|
"adminfront": "adminfront",
|
||||||
|
"devfront": "devfront",
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeUserRole(role string) string {
|
func normalizeUserRole(role string) string {
|
||||||
return domain.NormalizeRole(role)
|
return domain.NormalizeRole(role)
|
||||||
}
|
}
|
||||||
@@ -276,6 +281,34 @@ func isProtectedSystemClient(client domain.HydraClient) bool {
|
|||||||
return isProtectedSystemClientID(client.ClientID)
|
return isProtectedSystemClientID(client.ClientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isReservedSystemClientAlias(client domain.HydraClient) bool {
|
||||||
|
ownerID, reserved := reservedSystemClientOwnerID(client.ClientName)
|
||||||
|
if !reserved {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !strings.EqualFold(strings.TrimSpace(client.ClientID), ownerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHiddenSystemClient(client domain.HydraClient) bool {
|
||||||
|
return isProtectedSystemClient(client) || isReservedSystemClientAlias(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reservedSystemClientOwnerID(name string) (string, bool) {
|
||||||
|
ownerID, ok := reservedSystemClientNames[strings.ToLower(strings.TrimSpace(name))]
|
||||||
|
return ownerID, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateReservedSystemClientName(clientID, name string) error {
|
||||||
|
ownerID, reserved := reservedSystemClientOwnerID(name)
|
||||||
|
if !reserved {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(clientID), ownerID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("forbidden: reserved system client name")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
if (!ok || profile == nil) && h.Auth != nil {
|
if (!ok || profile == nil) && h.Auth != nil {
|
||||||
@@ -570,7 +603,7 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
items := make([]clientSummary, 0, len(clients))
|
items := make([]clientSummary, 0, len(clients))
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if isProtectedSystemClient(client) {
|
if isHiddenSystemClient(client) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +654,7 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if isProtectedSystemClient(*client) {
|
if isHiddenSystemClient(*client) {
|
||||||
return errorJSON(c, fiber.StatusNotFound, "client not found")
|
return errorJSON(c, fiber.StatusNotFound, "client not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +732,7 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if isProtectedSystemClient(*current) {
|
if isHiddenSystemClient(*current) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,6 +825,9 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
name = clientID
|
name = clientID
|
||||||
}
|
}
|
||||||
|
if err := validateReservedSystemClientName(clientID, name); err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusForbidden, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
redirectURIs := derefSlice(req.RedirectURIs, nil)
|
redirectURIs := derefSlice(req.RedirectURIs, nil)
|
||||||
if len(redirectURIs) == 0 {
|
if len(redirectURIs) == 0 {
|
||||||
@@ -927,7 +963,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if isProtectedSystemClient(*current) {
|
if isHiddenSystemClient(*current) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,6 +1048,9 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
TokenEndpointAuthMethod: resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod),
|
TokenEndpointAuthMethod: resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod),
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusForbidden, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
h.setAuditDetailsExtra(c, map[string]any{
|
h.setAuditDetailsExtra(c, map[string]any{
|
||||||
"action": "UPDATE_CLIENT",
|
"action": "UPDATE_CLIENT",
|
||||||
@@ -1062,7 +1101,7 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if isProtectedSystemClient(*current) {
|
if isHiddenSystemClient(*current) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1301,7 +1340,7 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if isProtectedSystemClient(*current) {
|
if isHiddenSystemClient(*current) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: protected system client")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1502,7 +1541,7 @@ func (h *DevHandler) GetStats(c *fiber.Ctx) error {
|
|||||||
var totalClients int64
|
var totalClients int64
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if isProtectedSystemClient(client) {
|
if isHiddenSystemClient(client) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isSuperAdmin {
|
if isSuperAdmin {
|
||||||
|
|||||||
@@ -124,6 +124,86 @@ func TestListClients_Success(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateClient_ReservedSystemNameForbidden(t *testing.T) {
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
t.Fatalf("hydra should not be called when reserved system name is rejected")
|
||||||
|
return 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: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Post("/api/v1/dev/clients", h.CreateClient)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "AdminFront",
|
||||||
|
"type": "pkce",
|
||||||
|
"redirectUris": []string{"http://localhost/cb"},
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateClient_ReservedSystemNameForbidden(t *testing.T) {
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"client_id": "client-1",
|
||||||
|
"client_name": "App One",
|
||||||
|
"redirect_uris": []string{
|
||||||
|
"http://localhost/cb",
|
||||||
|
},
|
||||||
|
"grant_types": []string{"authorization_code", "refresh_token"},
|
||||||
|
"response_types": []string{"code"},
|
||||||
|
"scope": "openid profile email offline_access",
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"metadata": map[string]any{"status": "active"},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
|
||||||
|
t.Fatalf("hydra update should not be called when reserved system name is rejected")
|
||||||
|
}
|
||||||
|
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: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Put("/api/v1/dev/clients/:id", h.UpdateClient)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "DevFront",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
func TestListClients_ProtectedSystemClientHidden(t *testing.T) {
|
func TestListClients_ProtectedSystemClientHidden(t *testing.T) {
|
||||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
if r.URL.Path == "/clients" {
|
if r.URL.Path == "/clients" {
|
||||||
@@ -162,6 +242,82 @@ func TestListClients_ProtectedSystemClientHidden(t *testing.T) {
|
|||||||
assert.Equal(t, "client-1", res.Items[0].ID)
|
assert.Equal(t, "client-1", res.Items[0].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListClients_ReservedSystemNameAliasHidden(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": "adminfront", "client_name": "AdminFront", "metadata": map[string]interface{}{"status": "active"}},
|
||||||
|
{"client_id": "4f2c9fd6-1111-2222-3333-444444444444", "client_name": "AdminFront", "metadata": map[string]interface{}{"status": "active"}},
|
||||||
|
{"client_id": "devfront", "client_name": "DevFront", "metadata": map[string]interface{}{"status": "active"}},
|
||||||
|
{"client_id": "7d2c9fd6-1111-2222-3333-444444444444", "client_name": "DevFront", "metadata": map[string]interface{}{"status": "active"}},
|
||||||
|
{"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 result clientListResponse
|
||||||
|
_ = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
assert.Len(t, result.Items, 3)
|
||||||
|
assert.Equal(t, "adminfront", result.Items[0].ID)
|
||||||
|
assert.Equal(t, "devfront", result.Items[1].ID)
|
||||||
|
assert.Equal(t, "client-1", result.Items[2].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetClient_ReservedSystemNameAliasHidden(t *testing.T) {
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.URL.Path == "/clients/4f2c9fd6-1111-2222-3333-444444444444" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||||
|
"client_id": "4f2c9fd6-1111-2222-3333-444444444444",
|
||||||
|
"client_name": "AdminFront",
|
||||||
|
"metadata": map[string]interface{}{"status": "active"},
|
||||||
|
}), 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: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Get("/api/v1/dev/clients/:id", h.GetClient)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/4f2c9fd6-1111-2222-3333-444444444444", nil)
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateClientStatus_Success(t *testing.T) {
|
func TestUpdateClientStatus_Success(t *testing.T) {
|
||||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
||||||
|
|||||||
Reference in New Issue
Block a user