From e7800f3fed6ed0ac797aa580f7e25fa37dbf9993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Eichhorn?= Date: Mon, 9 May 2022 11:06:53 +0200 Subject: [PATCH 1/5] Update helpers.py fixes #183 (#patch) --- src/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers.py b/src/helpers.py index 4155dbf..ef1d4b0 100755 --- a/src/helpers.py +++ b/src/helpers.py @@ -103,7 +103,7 @@ def request(url, method, enable_5xx=False, payload=None): username = os.getenv("REQ_USERNAME") password = os.getenv("REQ_PASSWORD") if username and password: - auth = (username, password) + auth = (username.encode('utf-8'), password.encode('utf-8')) else: auth = None From ab9ddc4eff9f5b51fbc7687a47a8cf1b5808fd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Eichhorn?= Date: Tue, 5 Jul 2022 10:12:24 +0200 Subject: [PATCH 2/5] chore: add test for basic auth --- .github/workflows/build_and_test.yaml | 4 +++- test/resources/resources.yaml | 9 +++++++++ test/resources/sidecar.yaml | 24 +++++++++++++++--------- test/server/server.py | 14 ++++++++++++-- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index b402f42..0251656 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -97,6 +97,7 @@ jobs: kubectl cp sidecar:/tmp/absolute/absolute.txt /tmp/absolute.txt kubectl cp sidecar:/tmp/relative/relative.txt /tmp/relative.txt kubectl cp sidecar:/tmp/500.txt /tmp/500.txt || true + kubectl cp sidecar:/tmp/secured.txt /tmp/secured.txt echo "Downloading resource files from sidecar-5xx..." kubectl cp sidecar-5xx:/tmp-5xx/hello.world /tmp/5xx/hello.world @@ -118,6 +119,7 @@ jobs: diff test/kubelogo.png /tmp/secret-kubelogo.png && echo -n "This absolutely exists" | diff - /tmp/absolute.txt && echo -n "This relatively exists" | diff - /tmp/relative.txt && + echo -n "allowed" | diff - /tmp/secured.txt && [ ! -f /tmp/500.txt ] && echo "No 5xx file created" && ls /tmp/script_result && echo -n "Hello World!" | diff - /tmp/5xx/hello.world && @@ -126,4 +128,4 @@ jobs: echo -n "This absolutely exists" | diff - /tmp/5xx/absolute.txt && echo -n "This relatively exists" | diff - /tmp/5xx/relative.txt && echo -n "500" | diff - /tmp/5xx/500.txt && - ls /tmp/5xx/script_result + ls /tmp/5xx/script_result \ No newline at end of file diff --git a/test/resources/resources.yaml b/test/resources/resources.yaml index ee621cc..17e33ab 100644 --- a/test/resources/resources.yaml +++ b/test/resources/resources.yaml @@ -51,3 +51,12 @@ metadata: findme: "yup" data: 500.txt.url: "http://dummy-server/500" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: url-configmap-basic-auth + labels: + findme: "yup" +data: + secured.txt.url: "http://dummy-server/secured" \ No newline at end of file diff --git a/test/resources/sidecar.yaml b/test/resources/sidecar.yaml index f2f7543..41fdc0e 100644 --- a/test/resources/sidecar.yaml +++ b/test/resources/sidecar.yaml @@ -53,14 +53,20 @@ spec: mountPath: /opt/script.sh subPath: script.sh env: - - name: LABEL - value: "findme" - - name: FOLDER - value: /tmp/ - - name: RESOURCE - value: both - - name: SCRIPT - value: "/opt/script.sh" + - name: LABEL + value: "findme" + - name: FOLDER + value: /tmp/ + - name: RESOURCE + value: both + - name: SCRIPT + value: "/opt/script.sh" + - name: REQ_USERNAME + value: "se§ure" + - name: REQ_PASSWORD + value: "s§cröt" + - name: LOG_LEVEL + value: "DEBUG" volumes: - name: shared-volume emptyDir: {} @@ -129,4 +135,4 @@ spec: ports: - port: 80 targetPort: 80 - name: http + name: http \ No newline at end of file diff --git a/test/server/server.py b/test/server/server.py index 29f198e..0b2f325 100644 --- a/test/server/server.py +++ b/test/server/server.py @@ -1,8 +1,10 @@ -from fastapi import FastAPI -import uvicorn +from fastapi import Depends, FastAPI, Response +from fastapi.security import HTTPBasic, HTTPBasicCredentials app = FastAPI() +basic_auth_scheme = HTTPBasic() + @app.get("/", status_code=200) def read_root(): @@ -27,3 +29,11 @@ async def read_item(): @app.post("/503", status_code=503) async def read_item(): return 503 + + +@app.get("/secured", status_code=200) +async def read_secure_data(response: Response, auth: HTTPBasicCredentials = Depends(basic_auth_scheme)): + if auth.username != 'se§ure' or auth.password != 's§cröt': + response.status_code = 403 + return 'forbidden' + return 'allowed' \ No newline at end of file From 0962e180b8f289db7d95e38319be011c4398b184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Eichhorn?= Date: Tue, 5 Jul 2022 10:59:23 +0200 Subject: [PATCH 3/5] chore: improve waiting logic in tests --- .github/workflows/build_and_test.yaml | 16 ++++++++++++++-- test/resources/sidecar.yaml | 22 ++++++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 0251656..2555fd1 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -72,11 +72,23 @@ jobs: - name: Install Configmaps and Secrets run: | + wait_for_pod_log() { + while [[ $(kubectl logs $1 | grep $2) == "" ]]; do echo "waiting 5 more seconds for '$2' to appear in logs of pod '$1'..." && sleep 5; done + echo "Pod '$1' logs contains '$2'" + } # because the sidecar pods signal ready state before we actually opened up all watching subprocesses, we wait some more time - sleep 15 + sleep 20 echo "Installing resources..." kubectl apply -f "test/resources/resources.yaml" - sleep 15 + pods=("sidecar" "sidecar-5xx") + resources=("sample-configmap" "sample-secret-binary" "absolute-configmap" "relative-configmap" "url-configmap-500" "url-configmap-basic-auth" "sample-configmap") + for p in ${pods[*]}; do + for r in ${resources[*]}; do + wait_for_pod_log $p $r + done + done + # 5 more seconds after the last thing appeared in the logs. + sleep 5 - name: Retrieve pod logs run: | kubectl logs sidecar > /tmp/sidecar.log diff --git a/test/resources/sidecar.yaml b/test/resources/sidecar.yaml index 41fdc0e..714d964 100644 --- a/test/resources/sidecar.yaml +++ b/test/resources/sidecar.yaml @@ -92,16 +92,18 @@ spec: mountPath: /opt-5xx/script.sh subPath: script.sh env: - - name: LABEL - value: "findme" - - name: FOLDER - value: /tmp-5xx/ - - name: RESOURCE - value: both - - name: SCRIPT - value: "/opt-5xx/script.sh" - - name: ENABLE_5XX - value: "true" + - name: LABEL + value: "findme" + - name: FOLDER + value: /tmp-5xx/ + - name: RESOURCE + value: both + - name: SCRIPT + value: "/opt-5xx/script.sh" + - name: ENABLE_5XX + value: "true" + - name: LOG_LEVEL + value: "DEBUG" volumes: - name: shared-volume emptyDir: {} From 0b273685ea11690a1609d080637aee99ca2efeee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Eichhorn?= Date: Tue, 5 Jul 2022 12:29:33 +0200 Subject: [PATCH 4/5] feat: make basic auth encoding configurable The RFC confirms the default encoding is undefined. So we make it configurable so users can match their servers decoding implementation. --- README.md | 3 ++- src/helpers.py | 12 ++++++++---- test/resources/sidecar.yaml | 2 ++ test/server/server.py | 14 +++++++++----- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fdf4dcd..8e0fc3a 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ If the filename ends with `.url` suffix, the content will be processed as a URL | `REQ_TIMEOUT` | How many seconds to wait for the server to send data before giving up for `.url` triggered requests or requests to `REQ_URI` (does not apply to k8s api requests) | false | `10` | float | | `REQ_USERNAME` | Username to use for basic authentication for requests to `REQ_URL` and for `*.url` triggered requests | false | - | string | | `REQ_PASSWORD` | Password to use for basic authentication for requests to `REQ_URL` and for `*.url` triggered requests | false | - | string | +| `REQ_BASIC_AUTH_ENCODING` | Which encoding to use for username and password as [by default it's undefined](https://datatracker.ietf.org/doc/html/rfc7617) (e.g. `utf-8`). | false | `latin1` | string | | `SCRIPT` | Absolute path to shell script to execute after a configmap got reloaded. It runs before calls to `REQ_URI` | false | - | string | | `ERROR_THROTTLE_SLEEP` | How many seconds to wait before watching resources again when an error occurs | false | `5` | integer | | `SKIP_TLS_VERIFY` | Set to `true` to skip tls verification for kube api calls | false | - | boolean | @@ -83,4 +84,4 @@ If the filename ends with `.url` suffix, the content will be processed as a URL | `IGNORE_ALREADY_PROCESSED` | Ignore already processed resource version. Avoid numerous checks on same unchanged resource. req kubernetes api >= v1.19 | false | `false` | boolean | | `LOG_LEVEL` | Set the logging level. (DEBUG, INFO, WARN, ERROR, CRITICAL) | false | `INFO` | string | | `LOG_FORMAT` | Set a log format. (JSON or LOGFMT) | false | `JSON` | string | -| `LOG_TZ` | Set the log timezone. (LOCAL or UTC) | false | `LOCAL` | string | +| `LOG_TZ` | Set the log timezone. (LOCAL or UTC) | false | `LOCAL` | string | \ No newline at end of file diff --git a/src/helpers.py b/src/helpers.py index ef1d4b0..2fa34d0 100755 --- a/src/helpers.py +++ b/src/helpers.py @@ -5,19 +5,22 @@ import os import subprocess from datetime import datetime -from logger import get_logger import requests from requests.adapters import HTTPAdapter +from requests.auth import HTTPBasicAuth from requests.packages.urllib3.util.retry import Retry +from logger import get_logger + CONTENT_TYPE_TEXT = "ascii" CONTENT_TYPE_BASE64_BINARY = "binary" REQ_RETRY_TOTAL = 5 if os.getenv("REQ_RETRY_TOTAL") is None else int(os.getenv("REQ_RETRY_TOTAL")) REQ_RETRY_CONNECT = 10 if os.getenv("REQ_RETRY_CONNECT") is None else int(os.getenv("REQ_RETRY_CONNECT")) REQ_RETRY_READ = 5 if os.getenv("REQ_RETRY_READ") is None else int(os.getenv("REQ_RETRY_READ")) -REQ_RETRY_BACKOFF_FACTOR = 1.1 if os.getenv("REQ_RETRY_BACKOFF_FACTOR") is None else float(os.getenv("REQ_RETRY_BACKOFF_FACTOR")) +REQ_RETRY_BACKOFF_FACTOR = 1.1 if os.getenv("REQ_RETRY_BACKOFF_FACTOR") is None else float( + os.getenv("REQ_RETRY_BACKOFF_FACTOR")) REQ_TIMEOUT = 10 if os.getenv("REQ_TIMEOUT") is None else float(os.getenv("REQ_TIMEOUT")) # Tune default timeouts as outlined in @@ -102,8 +105,9 @@ def request(url, method, enable_5xx=False, payload=None): username = os.getenv("REQ_USERNAME") password = os.getenv("REQ_PASSWORD") + encoding = 'latin1' if not os.getenv("REQ_BASIC_AUTH_ENCODING") else os.getenv("REQ_BASIC_AUTH_ENCODING") if username and password: - auth = (username.encode('utf-8'), password.encode('utf-8')) + auth = HTTPBasicAuth(username.encode(encoding), password.encode(encoding)) else: auth = None @@ -165,4 +169,4 @@ def execute(script_path): logger.debug(f"Script stderr: {result.stderr}") logger.debug(f"Script exit code: {result.returncode}") except subprocess.CalledProcessError as e: - logger.error(f"Script failed with error: {e}") + logger.error(f"Script failed with error: {e}") \ No newline at end of file diff --git a/test/resources/sidecar.yaml b/test/resources/sidecar.yaml index 714d964..f639009 100644 --- a/test/resources/sidecar.yaml +++ b/test/resources/sidecar.yaml @@ -104,6 +104,8 @@ spec: value: "true" - name: LOG_LEVEL value: "DEBUG" + - name: REQ_BASIC_AUTH_ENCODING + value: 'ascii' volumes: - name: shared-volume emptyDir: {} diff --git a/test/server/server.py b/test/server/server.py index 0b2f325..23b7971 100644 --- a/test/server/server.py +++ b/test/server/server.py @@ -1,4 +1,4 @@ -from fastapi import Depends, FastAPI, Response +from fastapi import Depends, FastAPI, status, HTTPException from fastapi.security import HTTPBasic, HTTPBasicCredentials app = FastAPI() @@ -32,8 +32,12 @@ async def read_item(): @app.get("/secured", status_code=200) -async def read_secure_data(response: Response, auth: HTTPBasicCredentials = Depends(basic_auth_scheme)): - if auth.username != 'se§ure' or auth.password != 's§cröt': - response.status_code = 403 - return 'forbidden' +async def read_secure_data(auth: HTTPBasicCredentials = Depends(basic_auth_scheme)): + if auth.username != 'foo' or auth.password != 'bar': + print(f"wrong auth: ${auth.username} : ${auth.password}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) return 'allowed' \ No newline at end of file From 8193ead75aa82c3edfb47b9b00faee3994b8573f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Eichhorn?= Date: Tue, 8 Nov 2022 11:44:13 +0100 Subject: [PATCH 5/5] refine test for #183 --- .github/workflows/build_and_test.yaml | 2 ++ test/resources/sidecar.yaml | 9 +++++---- test/server/server.py | 10 ++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 2555fd1..34b247f 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -121,6 +121,7 @@ jobs: kubectl cp sidecar-5xx:/tmp/absolute/absolute.txt /tmp/5xx/absolute.txt kubectl cp sidecar-5xx:/tmp-5xx/relative/relative.txt /tmp/5xx/relative.txt kubectl cp sidecar-5xx:/tmp-5xx/500.txt /tmp/5xx/500.txt + kubectl cp sidecar-5xx:/tmp-5xx/secured.txt /tmp/5xx/secured.txt - name: Verify files run: | @@ -132,6 +133,7 @@ jobs: echo -n "This absolutely exists" | diff - /tmp/absolute.txt && echo -n "This relatively exists" | diff - /tmp/relative.txt && echo -n "allowed" | diff - /tmp/secured.txt && + echo -n '{"detail":"Not authenticated"}' | diff - /tmp/5xx/secured.txt && [ ! -f /tmp/500.txt ] && echo "No 5xx file created" && ls /tmp/script_result && echo -n "Hello World!" | diff - /tmp/5xx/hello.world && diff --git a/test/resources/sidecar.yaml b/test/resources/sidecar.yaml index f639009..4d3cbb0 100644 --- a/test/resources/sidecar.yaml +++ b/test/resources/sidecar.yaml @@ -62,9 +62,12 @@ spec: - name: SCRIPT value: "/opt/script.sh" - name: REQ_USERNAME - value: "se§ure" + value: "user1" - name: REQ_PASSWORD - value: "s§cröt" + value: "abcdefghijklmnopqrstuvwxyz" + - name: REQ_BASIC_AUTH_ENCODING + # the python server we're using for the tests expects ascii encoding of basic auth credentials, hence we can't use non-ascii characters in the password or username + value: "ascii" - name: LOG_LEVEL value: "DEBUG" volumes: @@ -104,8 +107,6 @@ spec: value: "true" - name: LOG_LEVEL value: "DEBUG" - - name: REQ_BASIC_AUTH_ENCODING - value: 'ascii' volumes: - name: shared-volume emptyDir: {} diff --git a/test/server/server.py b/test/server/server.py index 23b7971..c6ea0d3 100644 --- a/test/server/server.py +++ b/test/server/server.py @@ -1,5 +1,7 @@ from fastapi import Depends, FastAPI, status, HTTPException +from fastapi.logger import logger from fastapi.security import HTTPBasic, HTTPBasicCredentials +from starlette.responses import PlainTextResponse app = FastAPI() @@ -31,13 +33,13 @@ async def read_item(): return 503 -@app.get("/secured", status_code=200) +@app.get("/secured", status_code=200, response_class=PlainTextResponse) async def read_secure_data(auth: HTTPBasicCredentials = Depends(basic_auth_scheme)): - if auth.username != 'foo' or auth.password != 'bar': - print(f"wrong auth: ${auth.username} : ${auth.password}") + if auth.username != 'user1' or auth.password != 'abcdefghijklmnopqrstuvwxyz': + logger.warning("[WARN] wrong auth: %s : %s ", auth.username, auth.password) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", + detail=f"Incorrect user (${auth.username}) or password (${auth.password})", headers={"WWW-Authenticate": "Basic"}, ) return 'allowed' \ No newline at end of file