Skip to content

Commit 5bbc403

Browse files
Merge branch 'main' into fix-418-interactive-sql-logging
2 parents 2ddb55b + c938fad commit 5bbc403

15 files changed

Lines changed: 297 additions & 29 deletions

mgradm/cmd/distro/cp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func registerDistro(connection *api.ConnectionDetails, distro *types.Distributio
6161
"installType": distro.InstallType,
6262
}
6363

64-
_, err = client.Post("kickstart/tree/create", data)
64+
_, err = client.PostChecked("kickstart/tree/create", "api.kickstart.tree.create", data)
6565
if err != nil {
6666
return utils.Errorf(err, L("unable to register the distribution. Manual distro registration is required"))
6767
}

mgradm/shared/podman/podman.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -585,22 +585,6 @@ func RunSplitContainerSettings(serverImage string, dbHost string, reportDBHost s
585585
[]string{"bash", "-e", "-c", scriptBuilder.String()})
586586
}
587587

588-
// RunConfigPgsl setup postgres container.
589-
func RunConfigPgsl(pgsqlImage string) error {
590-
podmanArgs := []string{
591-
"--security-opt", "label=disable",
592-
"--secret", fmt.Sprintf("%s,type=mount,target=%s", podman.DBCASecret, ssl.DBCAContainerPath),
593-
"--secret", fmt.Sprintf("%s,type=mount,uid=999,mode=0400,target=%s", podman.DBSSLKeySecret, ssl.DBCertKeyPath),
594-
"--secret", fmt.Sprintf("%s,type=mount,target=%s", podman.DBSSLCertSecret, ssl.DBCertPath),
595-
"--entrypoint", "/docker-entrypoint-initdb.d/uyuni-postgres-config.sh",
596-
}
597-
if err := podman.RunContainer("uyuni-db-config", pgsqlImage, utils.PgsqlRequiredVolumeMounts,
598-
podmanArgs, []string{}); err != nil {
599-
return err
600-
}
601-
return systemd.RestartService(podman.DBService)
602-
}
603-
604588
// CallCloudGuestRegistryAuth calls cloudguestregistryauth if it is available.
605589
func CallCloudGuestRegistryAuth() error {
606590
cloudguestregistryauth := "cloudguestregistryauth"
@@ -729,5 +713,5 @@ func configureDBContainer(
729713
db.Host,
730714
)
731715
}
732-
return RunConfigPgsl(pgsqlImage)
716+
return nil
733717
}

mgradm/shared/templates/pgsqlMigrateScriptTemplate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ set -e -x
1515
1616
{{ if .ReportDBHost }}
1717
sed 's/^report_db_host = .*/report_db_host = {{ .ReportDBHost }}/' -i /etc/rhn/rhn.conf;
18+
sed 's/^report_db_ssl_enabled = .*/report_db_ssl_enabled = 0/' -i /etc/rhn/rhn.conf;
1819
{{ end }}
1920
2021
{{ if .DBHost }}
2122
sed 's/^db_host = .*/db_host = {{ .DBHost }}/' -i /etc/rhn/rhn.conf;
23+
sed 's/^db_ssl_enabled = .*/db_ssl_enabled = 0/' -i /etc/rhn/rhn.conf;
2224
{{ end }}
2325
2426
echo "DONE"`

mgrctl/cmd/ssh/knownhost/remove.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func removeKnownHost(client *api.APIClient, hostname string, port string) error
2424
path := "admin/ssh/removeKnownHost?" + params.Encode()
2525

2626
var data map[string]interface{}
27-
res, err := api.Post[interface{}](client, path, data)
27+
res, err := api.PostChecked[interface{}](client, path, "api.admin.ssh.remove_known_host", data)
2828
if err != nil {
2929
return utils.Errorf(err, L("error in query '%s'"), path)
3030
}

shared/api/api.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2024 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -225,7 +225,7 @@ func (c *APIClient) ValidateCreds() bool {
225225
return err == nil
226226
}
227227

228-
// Post issues a POST HTTP request to the API target
228+
// Post issues a POST HTTP request to the API target, without validating the endpoint.
229229
//
230230
// `path` specifies an API endpoint
231231
// `data` contains a map of values to add to the POST query. `data` are serialized to the JSON
@@ -254,6 +254,26 @@ func (c *APIClient) Post(path string, data map[string]interface{}) (*http.Respon
254254
return res, nil
255255
}
256256

257+
// PostChecked validates the API endpoint before issuing a POST request.
258+
func (c *APIClient) PostChecked(path string, endpoint string, data map[string]interface{}) (*http.Response, error) {
259+
if err := c.validateEndpoint(endpoint); err != nil {
260+
return nil, err
261+
}
262+
return c.Post(path, data)
263+
}
264+
265+
// PostChecked issues a validated POST request to the API target using the client and decodes the response.
266+
func PostChecked[T interface{}](
267+
client *APIClient, path string, namespace string, data map[string]interface{},
268+
) (*APIResponse[T], error) {
269+
res, err := client.PostChecked(path, namespace, data)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
return decodeAPIResponse[T](res)
275+
}
276+
257277
// Get issues GET HTTP request to the API target
258278
//
259279
// `path` specifies API endpoint together with query options
@@ -313,10 +333,32 @@ func Get[T interface{}](client *APIClient, path string) (*APIResponse[T], error)
313333
return nil, err
314334
}
315335

336+
return decodeAPIResponse[T](res)
337+
}
338+
339+
// GetChecked validates the API namespace before issuing a GET request.
340+
func (c *APIClient) GetChecked(path string, namespace string) (*http.Response, error) {
341+
if err := c.validateEndpoint(namespace); err != nil {
342+
return nil, err
343+
}
344+
return c.Get(path)
345+
}
346+
347+
// GetChecked issues a validated GET request to the API using the client and decodes the response.
348+
func GetChecked[T interface{}](client *APIClient, path string, endpoint string) (*APIResponse[T], error) {
349+
res, err := client.GetChecked(path, endpoint)
350+
if err != nil {
351+
return nil, err
352+
}
353+
354+
return decodeAPIResponse[T](res)
355+
}
356+
357+
func decodeAPIResponse[T interface{}](res *http.Response) (*APIResponse[T], error) {
316358
defer res.Body.Close()
317359

318360
var response APIResponse[T]
319-
if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
361+
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
320362
return nil, err
321363
}
322364

shared/api/namespaces.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package api
6+
7+
import (
8+
"encoding/json"
9+
"errors"
10+
11+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
12+
)
13+
14+
const unsupportedFunctionError = "This function is currently not supported by the server."
15+
16+
// ValidateNamespace checks if the server advertises the requested API namespace.
17+
func ValidateNamespace(client *APIClient, namespace string) error {
18+
if namespace == "" {
19+
return nil
20+
}
21+
if err := client.loadValidNamespaces(); err != nil {
22+
return err
23+
}
24+
if _, ok := client.validNamespaces[namespace]; ok {
25+
return nil
26+
}
27+
return errors.New(L(unsupportedFunctionError))
28+
}
29+
30+
func (c *APIClient) validateEndpoint(endpoint string) error {
31+
if c.AuthCookie == nil || endpoint == "" {
32+
return nil
33+
}
34+
return ValidateNamespace(c, endpoint)
35+
}
36+
37+
func (c *APIClient) loadValidNamespaces() error {
38+
if c.validNamespaces != nil {
39+
return nil
40+
}
41+
42+
res, err := c.Get("access/listNamespaces")
43+
if err != nil {
44+
return err
45+
}
46+
defer res.Body.Close()
47+
48+
var response APIResponse[[]NamespaceAccess]
49+
if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
50+
return err
51+
}
52+
if !response.Success {
53+
return errors.New(response.Message)
54+
}
55+
56+
c.validNamespaces = map[string]struct{}{}
57+
for _, namespace := range response.Result {
58+
if namespace.Namespace != "" {
59+
c.validNamespaces[namespace.Namespace] = struct{}{}
60+
}
61+
}
62+
return nil
63+
}

shared/api/namespaces_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package api
6+
7+
import (
8+
"net/http"
9+
"testing"
10+
11+
"github.com/uyuni-project/uyuni-tools/shared/api/mocks"
12+
"github.com/uyuni-project/uyuni-tools/shared/testutils"
13+
)
14+
15+
const removeKnownHostNamespace = "api.admin.ssh.remove_known_host"
16+
17+
func namespaceListResponse(namespace string) string {
18+
return `{"success":true,"result":[{"namespace":"` + namespace + `","access_mode":"W"}]}`
19+
}
20+
21+
func TestPostChecked(t *testing.T) {
22+
tests := []struct {
23+
name string
24+
path string
25+
endpoint string
26+
apiPath string
27+
expectedError string
28+
targetCalled bool
29+
}{
30+
{
31+
name: "Test advertised endpoint",
32+
path: "admin/ssh/removeKnownHost",
33+
endpoint: removeKnownHostNamespace,
34+
apiPath: "/rhn/manager/api/admin/ssh/removeKnownHost",
35+
targetCalled: true,
36+
},
37+
{
38+
name: "Test unsupported endpoint",
39+
path: "admin/ssh/removeUnknownHost",
40+
endpoint: "api.admin.ssh.remove_unknown_host",
41+
apiPath: "/rhn/manager/api/admin/ssh/removeUnknownHost",
42+
expectedError: unsupportedFunctionError,
43+
},
44+
}
45+
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
client, err := Init(&ConnectionDetails{Server: "testServer", Cookie: "testCookie"})
49+
if err != nil {
50+
t.FailNow()
51+
}
52+
53+
targetCalled := false
54+
client.Client = &mocks.MockClient{DoFunc: func(req *http.Request) (*http.Response, error) {
55+
switch req.URL.Path {
56+
case "/rhn/manager/api/access/listNamespaces":
57+
return testutils.GetResponse(200, namespaceListResponse(removeKnownHostNamespace))
58+
case tt.apiPath:
59+
targetCalled = true
60+
return testutils.GetResponse(200, `{"success":true,"result":true}`)
61+
default:
62+
t.Errorf("Unexpected API path %s", req.URL.Path)
63+
return testutils.GetResponse(404, `{}`)
64+
}
65+
}}
66+
67+
errorMessage := ""
68+
if _, err := client.PostChecked(tt.path, tt.endpoint, nil); err != nil {
69+
errorMessage = err.Error()
70+
}
71+
72+
testutils.AssertStringContains(t, "Unexpected error message", errorMessage, tt.expectedError)
73+
testutils.AssertEquals(t, "Unexpected target call", tt.targetCalled, targetCalled)
74+
})
75+
}
76+
}
77+
78+
func TestGetChecked(t *testing.T) {
79+
tests := []struct {
80+
name string
81+
path string
82+
endpoint string
83+
apiPath string
84+
expectedError string
85+
targetCalled bool
86+
}{
87+
{
88+
name: "Test advertised endpoint",
89+
path: "org/getDetails?name=admin",
90+
endpoint: "api.org.get_details",
91+
apiPath: "/rhn/manager/api/org/getDetails",
92+
targetCalled: true,
93+
},
94+
{
95+
name: "Test unsupported endpoint",
96+
path: "org/getUnknownDetails?name=admin",
97+
endpoint: "api.org.get_unknown_details",
98+
apiPath: "/rhn/manager/api/org/getUnknownDetails",
99+
expectedError: unsupportedFunctionError,
100+
},
101+
}
102+
103+
for _, tt := range tests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
client, err := Init(&ConnectionDetails{Server: "testServer", Cookie: "testCookie"})
106+
if err != nil {
107+
t.FailNow()
108+
}
109+
110+
targetCalled := false
111+
client.Client = &mocks.MockClient{DoFunc: func(req *http.Request) (*http.Response, error) {
112+
switch req.URL.Path {
113+
case "/rhn/manager/api/access/listNamespaces":
114+
return testutils.GetResponse(200, namespaceListResponse("api.org.get_details"))
115+
case tt.apiPath:
116+
targetCalled = true
117+
return testutils.GetResponse(200, `{"success":true,"result":true}`)
118+
default:
119+
t.Errorf("Unexpected API path %s", req.URL.Path)
120+
return testutils.GetResponse(404, `{}`)
121+
}
122+
}}
123+
124+
errorMessage := ""
125+
if _, err := client.GetChecked(tt.path, tt.endpoint); err != nil {
126+
errorMessage = err.Error()
127+
}
128+
129+
testutils.AssertStringContains(t, "Unexpected error message", errorMessage, tt.expectedError)
130+
testutils.AssertEquals(t, "Unexpected target call", tt.targetCalled, targetCalled)
131+
})
132+
}
133+
}
134+
135+
func TestRawPostDoesNotValidateEndpoint(t *testing.T) {
136+
client, err := Init(&ConnectionDetails{Server: "testServer", Cookie: "testCookie"})
137+
if err != nil {
138+
t.FailNow()
139+
}
140+
141+
targetCalled := false
142+
client.Client = &mocks.MockClient{DoFunc: func(req *http.Request) (*http.Response, error) {
143+
testutils.AssertEquals(t, "Wrong URL called", req.URL.Path, "/rhn/manager/api/org/createFirst")
144+
targetCalled = true
145+
return testutils.GetResponse(200, `{"success":true,"result":true}`)
146+
}}
147+
148+
if _, err := client.Post("org/createFirst", nil); err != nil {
149+
t.Fatalf("Expected raw POST request to bypass endpoint validation: %v", err)
150+
}
151+
testutils.AssertTrue(t, "Expected raw target endpoint to be called", targetCalled)
152+
}

shared/api/org/createFirst.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2024 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -31,7 +31,7 @@ func CreateFirst(cnxDetails *api.ConnectionDetails, orgName string, admin *types
3131
"email": admin.Email,
3232
}
3333

34-
res, err := api.Post[types.Organization](client, "org/createFirst", data)
34+
res, err := api.PostChecked[types.Organization](client, "org/createFirst", "api.org.create_first", data)
3535
if err != nil {
3636
return nil, utils.Errorf(err, L("failed to create first user and organization"))
3737
}

shared/api/org/getDetails.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2024 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -23,7 +23,9 @@ func GetOrganizationDetails(cnxDetails *api.ConnectionDetails, orgName string) (
2323
if err != nil {
2424
return nil, utils.Errorf(err, L("failed to connect to the server"))
2525
}
26-
res, err := api.Get[types.Organization](client, fmt.Sprintf("org/getDetails?name=%s", orgName))
26+
res, err := api.GetChecked[types.Organization](
27+
client, fmt.Sprintf("org/getDetails?name=%s", orgName), "api.org.get_details",
28+
)
2729
if err != nil {
2830
return nil, utils.Errorf(err, L("failed to get organization details"))
2931
}

0 commit comments

Comments
 (0)