Skip to content

Commit 0012443

Browse files
authored
fix: ensure proper shell quoting for parameters (streamnative#837)
* fix(auth): ensure proper shell quoting for auth parameters * test(ci): add e2e test for quoted function details * fix(ci): ensure FUNCTION_NAME is not empty in verify.sh
1 parent 2587caf commit 0012443

8 files changed

Lines changed: 356 additions & 19 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
apiVersion: compute.functionmesh.io/v1alpha1
2+
kind: Function
3+
metadata:
4+
name: function-details-quoted-sample
5+
namespace: default
6+
spec:
7+
image: streamnative/pulsar-functions-java-sample:3.2.2.1
8+
className: org.apache.pulsar.functions.api.examples.ExclamationFunction
9+
replicas: 1
10+
logTopic: persistent://public/default/logging-function-details-quoted-logs
11+
funcConfig:
12+
timePartitionPattern: "yyyy-MM-dd/HH'h'-mm'm'"
13+
note: "quoted payload should survive shell parsing"
14+
input:
15+
topics:
16+
- persistent://public/default/input-function-details-quoted-topic
17+
typeClassName: java.lang.String
18+
output:
19+
topic: persistent://public/default/output-function-details-quoted-topic
20+
typeClassName: java.lang.String
21+
resources:
22+
requests:
23+
cpu: 50m
24+
memory: 1G
25+
limits:
26+
memory: 1.1G
27+
pulsar:
28+
pulsarConfig: "test-pulsar"
29+
tlsConfig:
30+
enabled: false
31+
allowInsecure: false
32+
hostnameVerification: true
33+
certSecretName: sn-platform-tls-broker
34+
certSecretKey: ""
35+
java:
36+
jar: /pulsar/examples/api-examples.jar
37+
clusterName: test
38+
autoAck: true
39+
---
40+
apiVersion: v1
41+
kind: ConfigMap
42+
metadata:
43+
name: test-pulsar
44+
data:
45+
webServiceURL: http://sn-platform-pulsar-broker.default.svc.cluster.local:8080
46+
brokerServiceURL: pulsar://sn-platform-pulsar-broker.default.svc.cluster.local:6650
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
#
20+
21+
set -e
22+
23+
E2E_DIR=$(dirname "$0")
24+
BASE_DIR=$(cd "${E2E_DIR}"/../../../../..; pwd)
25+
PULSAR_NAMESPACE=${PULSAR_NAMESPACE:-"default"}
26+
PULSAR_RELEASE_NAME=${PULSAR_RELEASE_NAME:-"sn-platform"}
27+
E2E_KUBECONFIG=${E2E_KUBECONFIG:-"/tmp/e2e-k8s.config"}
28+
MANIFESTS_FILE="${BASE_DIR}"/.ci/tests/integration/cases/quoted-function-details/manifests.yaml
29+
30+
source "${BASE_DIR}"/.ci/helm.sh
31+
32+
FUNCTION_NAME=function-details-quoted-sample
33+
STS_NAME=${FUNCTION_NAME}-function
34+
35+
if [ ! "$KUBECONFIG" ]; then
36+
export KUBECONFIG=${E2E_KUBECONFIG}
37+
fi
38+
39+
if [ -z "${FUNCTION_NAME}" ]; then
40+
echo "function name is empty"
41+
exit 1
42+
fi
43+
44+
cleanup() {
45+
kubectl delete -f "${MANIFESTS_FILE}" > /dev/null 2>&1 || true
46+
}
47+
48+
require_fragment() {
49+
fragment=$1
50+
message=$2
51+
52+
printf '%s' "${START_COMMAND}" | grep -F -- "${fragment}" > /dev/null 2>&1 || {
53+
echo "${message}"
54+
echo "${START_COMMAND}"
55+
exit 1
56+
}
57+
}
58+
59+
trap cleanup EXIT
60+
61+
kubectl apply -f "${MANIFESTS_FILE}" > /dev/null 2>&1
62+
63+
START_COMMAND=""
64+
for _ in $(seq 1 24); do
65+
START_COMMAND=$(kubectl get statefulset "${STS_NAME}" -o jsonpath='{.spec.template.spec.containers[0].command[2]}' 2> /dev/null || true)
66+
if [ -n "${START_COMMAND}" ]; then
67+
break
68+
fi
69+
sleep 5
70+
done
71+
72+
if [ -z "${START_COMMAND}" ]; then
73+
echo "statefulset command is not available"
74+
exit 1
75+
fi
76+
77+
unsafe_pattern_fragment=$(cat <<'EOF'
78+
timePartitionPattern\":\"yyyy-MM-dd/HH'h'-mm'm'
79+
EOF
80+
)
81+
escaped_h_fragment=$(cat <<'EOF'
82+
'"'"'h'"'"'
83+
EOF
84+
)
85+
escaped_m_fragment=$(cat <<'EOF'
86+
'"'"'m'"'"'
87+
EOF
88+
)
89+
90+
require_fragment "timePartitionPattern" "function details command does not include the quoted config key"
91+
if printf '%s' "${START_COMMAND}" | grep -F -- "${unsafe_pattern_fragment}" > /dev/null 2>&1; then
92+
echo "function details command still contains raw single quotes"
93+
echo "${START_COMMAND}"
94+
exit 1
95+
fi
96+
require_fragment "${escaped_h_fragment}" "function details command does not escape the quoted h segment"
97+
require_fragment "${escaped_m_fragment}" "function details command does not escape the quoted m segment"
98+
99+
verify_fm_result=$(ci::verify_function_mesh "${FUNCTION_NAME}" 2>&1)
100+
if [ $? -ne 0 ]; then
101+
echo "${verify_fm_result}"
102+
exit 1
103+
fi
104+
105+
verify_java_result=$(NAMESPACE=${PULSAR_NAMESPACE} CLUSTER=${PULSAR_RELEASE_NAME} ci::verify_exclamation_function \
106+
persistent://public/default/input-function-details-quoted-topic \
107+
persistent://public/default/output-function-details-quoted-topic \
108+
test-message \
109+
test-message! \
110+
10 2>&1)
111+
if [ $? -eq 0 ]; then
112+
echo "e2e-test: ok" | yq eval -
113+
else
114+
echo "${verify_java_result}"
115+
exit 1
116+
fi

.ci/tests/integration/e2e.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ verify:
133133
expected: expected.data.yaml
134134
- query: timeout 5m bash .ci/tests/integration/cases/java-function/verify.sh
135135
expected: expected.data.yaml
136+
- query: timeout 5m bash .ci/tests/integration/cases/quoted-function-details/verify.sh
137+
expected: expected.data.yaml
136138
- query: bash .ci/tests/integration/cases/java-function-vpa/verify.sh
137139
expected: expected.data.yaml
138140
- query: bash .ci/tests/integration/cases/reconciliation/verify.sh

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ index_build_*/
3737
config/crd/bases/vpa-v1-crd.yaml
3838
Listeners
3939
.tool-versions
40+
/tmp

api/compute/v1alpha1/common.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (o *OAuth2Config) GetMountFile() string {
127127
}
128128

129129
func (o *OAuth2Config) AuthenticationParameters() string {
130-
return fmt.Sprintf(`'{"credentials_url":"file://%s","privateKey":"%s","private_key":"%s","issuerUrl":"%s","issuer_url":"%s","audience":"%s","scope":"%s"}'`, o.GetMountFile(), o.GetMountFile(), o.GetMountFile(), o.IssuerURL, o.IssuerURL, o.Audience, o.Scope)
130+
return fmt.Sprintf(`{"credentials_url":"file://%s","privateKey":"%s","private_key":"%s","issuerUrl":"%s","issuer_url":"%s","audience":"%s","scope":"%s"}`, o.GetMountFile(), o.GetMountFile(), o.GetMountFile(), o.IssuerURL, o.IssuerURL, o.Audience, o.Scope)
131131
}
132132

133133
type GenericAuth struct {

controllers/spec/common.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -831,12 +831,16 @@ func getOAuth2MountFile(authConfig *v1alpha1.OAuth2Config, mountPath string) str
831831
return fmt.Sprintf("%s/%s", mountPath, authConfig.KeySecretKey)
832832
}
833833

834+
func shellQuoteLiteral(value string) string {
835+
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
836+
}
837+
834838
func makeOAuth2AuthenticationParameters(authConfig *v1alpha1.OAuth2Config, mountPath string) string {
835839
if authConfig == nil {
836840
return ""
837841
}
838842
credentialsFile := getOAuth2MountFile(authConfig, mountPath)
839-
return fmt.Sprintf(`'{"credentials_url":"file://%s","privateKey":"%s","private_key":"%s","issuerUrl":"%s","issuer_url":"%s","audience":"%s","scope":"%s"}'`,
843+
return fmt.Sprintf(`{"credentials_url":"file://%s","privateKey":"%s","private_key":"%s","issuerUrl":"%s","issuer_url":"%s","audience":"%s","scope":"%s"}`,
840844
credentialsFile, credentialsFile, credentialsFile, authConfig.IssuerURL, authConfig.IssuerURL, authConfig.Audience, authConfig.Scope)
841845
}
842846

@@ -859,14 +863,14 @@ func getPulsarAdminCommandWithEnv(authProvided, tlsProvided bool, tlsConfig TLSC
859863
"--auth-plugin",
860864
OAuth2AuthenticationPlugin,
861865
"--auth-params",
862-
makeOAuth2AuthenticationParameters(authConfig.OAuth2Config, oauth2MountPath),
866+
shellQuoteLiteral(makeOAuth2AuthenticationParameters(authConfig.OAuth2Config, oauth2MountPath)),
863867
}...)
864868
} else if authConfig.GenericAuth != nil {
865869
args = append(args, []string{
866870
"--auth-plugin",
867871
authConfig.GenericAuth.ClientAuthenticationPlugin,
868872
"--auth-params",
869-
"'" + authConfig.GenericAuth.ClientAuthenticationParameters + "'",
873+
shellQuoteLiteral(authConfig.GenericAuth.ClientAuthenticationParameters),
870874
}...)
871875
}
872876
} else if authProvided {
@@ -960,13 +964,13 @@ func getPulsarctlCommandWithEnv(authProvided, tlsProvided bool, tlsConfig TLSCon
960964
"oauth2",
961965
"activate",
962966
"--auth-params",
963-
"'" + authConfig.GenericAuth.ClientAuthenticationParameters + "'",
967+
shellQuoteLiteral(authConfig.GenericAuth.ClientAuthenticationParameters),
964968
"|| true ) &&",
965969
PulsarctlExecutableFile,
966970
"--auth-plugin",
967971
authConfig.GenericAuth.ClientAuthenticationPlugin,
968972
"--auth-params",
969-
"'" + authConfig.GenericAuth.ClientAuthenticationParameters + "'",
973+
shellQuoteLiteral(authConfig.GenericAuth.ClientAuthenticationParameters),
970974
"--admin-service-url",
971975
"$" + envNames.webServiceURL,
972976
}
@@ -1610,7 +1614,7 @@ func getSharedArgs(details, clusterName, uid string, authProvided bool, tlsProvi
16101614
"--function_version",
16111615
"0",
16121616
"--function_details",
1613-
"'" + details + "'", //in json format
1617+
shellQuoteLiteral(details), // in json format
16141618
"--pulsar_serviceurl",
16151619
"$brokerServiceURL",
16161620
"--max_buffered_tuples",
@@ -1631,13 +1635,13 @@ func getSharedArgs(details, clusterName, uid string, authProvided bool, tlsProvi
16311635
"--client_auth_plugin",
16321636
OAuth2AuthenticationPlugin,
16331637
"--client_auth_params",
1634-
authConfig.OAuth2Config.AuthenticationParameters()}...)
1638+
shellQuoteLiteral(authConfig.OAuth2Config.AuthenticationParameters())}...)
16351639
} else if authConfig.GenericAuth != nil {
16361640
args = append(args, []string{
16371641
"--client_auth_plugin",
16381642
authConfig.GenericAuth.ClientAuthenticationPlugin,
16391643
"--client_auth_params",
1640-
"'" + authConfig.GenericAuth.ClientAuthenticationParameters + "'"}...)
1644+
shellQuoteLiteral(authConfig.GenericAuth.ClientAuthenticationParameters)}...)
16411645
}
16421646
} else if authProvided {
16431647
args = append(args, []string{

controllers/spec/common_test.go

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,11 @@ func TestGetDownloadCommand(t *testing.T) {
205205
"oauth2",
206206
"activate",
207207
"--auth-params",
208-
"'auth-params'",
208+
shellQuoteLiteral("auth-params"),
209209
"|| true ) &&",
210210
PulsarctlExecutableFile,
211211
"--auth-plugin", "auth-plugin",
212-
"--auth-params", "'auth-params'",
212+
"--auth-params", shellQuoteLiteral("auth-params"),
213213
"--admin-service-url", "$webServiceURL",
214214
"--tls-allow-insecure=true",
215215
"--tls-enable-hostname-verification=false",
@@ -239,7 +239,7 @@ func TestGetDownloadCommand(t *testing.T) {
239239
PulsarAdminExecutableFile,
240240
"--admin-url", "$webServiceURL",
241241
"--auth-plugin", OAuth2AuthenticationPlugin,
242-
"--auth-params", testOauth2.AuthenticationParameters(),
242+
"--auth-params", shellQuoteLiteral(testOauth2.AuthenticationParameters()),
243243
"packages", "download", "function://public/default/test@v1", "--path", "function-package.jar",
244244
},
245245
false,
@@ -252,7 +252,7 @@ func TestGetDownloadCommand(t *testing.T) {
252252
PulsarAdminExecutableFile,
253253
"--admin-url", "$webServiceURL",
254254
"--auth-plugin", "auth-plugin",
255-
"--auth-params", "'auth-params'",
255+
"--auth-params", shellQuoteLiteral("auth-params"),
256256
"packages", "download", "sink://public/default/test@v1", "--path", "sink-package.jar",
257257
},
258258
false,
@@ -335,6 +335,67 @@ func TestGetDownloadCommand(t *testing.T) {
335335
}
336336
}
337337

338+
func TestShellQuoteLiteral(t *testing.T) {
339+
testCases := []struct {
340+
name string
341+
input string
342+
expected string
343+
}{
344+
{
345+
name: "empty",
346+
input: "",
347+
expected: "''",
348+
},
349+
{
350+
name: "plain",
351+
input: "auth-params",
352+
expected: "'auth-params'",
353+
},
354+
{
355+
name: "embedded single quote",
356+
input: "a'b",
357+
expected: `'a'"'"'b'`,
358+
},
359+
{
360+
name: "time partition pattern",
361+
input: "yyyy-MM-dd/HH'h'-mm'm'",
362+
expected: `'yyyy-MM-dd/HH'"'"'h'"'"'-mm'"'"'m'"'"''`,
363+
},
364+
}
365+
366+
for _, tc := range testCases {
367+
t.Run(tc.name, func(t *testing.T) {
368+
assert.Equal(t, tc.expected, shellQuoteLiteral(tc.input))
369+
})
370+
}
371+
}
372+
373+
func TestGetPulsarAdminCommandWithQuotedAuthParams(t *testing.T) {
374+
authConfig := &v1alpha1.AuthConfig{
375+
GenericAuth: &v1alpha1.GenericAuth{
376+
ClientAuthenticationPlugin: "auth-plugin",
377+
ClientAuthenticationParameters: `{"token":"a'b"}`,
378+
},
379+
}
380+
381+
command := getPulsarAdminCommand(false, false, nil, authConfig)
382+
assert.Equal(t, "auth-plugin", command[4])
383+
assert.Equal(t, shellQuoteLiteral(`{"token":"a'b"}`), command[6])
384+
}
385+
386+
func TestGetPulsarctlCommandWithQuotedAuthParams(t *testing.T) {
387+
authConfig := &v1alpha1.AuthConfig{
388+
GenericAuth: &v1alpha1.GenericAuth{
389+
ClientAuthenticationPlugin: "auth-plugin",
390+
ClientAuthenticationParameters: `{"token":"a'b"}`,
391+
},
392+
}
393+
394+
command := getPulsarctlCommand(false, false, nil, authConfig)
395+
assert.Equal(t, shellQuoteLiteral(`{"token":"a'b"}`), command[5])
396+
assert.Equal(t, shellQuoteLiteral(`{"token":"a'b"}`), command[11])
397+
}
398+
338399
func TestGetFunctionRunnerImage(t *testing.T) {
339400
javaRuntime := v1alpha1.Runtime{Java: &v1alpha1.JavaRuntime{
340401
Jar: "test.jar",

0 commit comments

Comments
 (0)