diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index b402f427..c8e91c4b 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -68,6 +68,8 @@ jobs: wait_for_pod_ready "sidecar" wait_for_pod_ready "sidecar-5xx" + wait_for_pod_ready "sidecar-api-key" + wait_for_pod_ready "sidecar-api-key-fails" wait_for_pod_ready "dummy-server-pod" - name: Install Configmaps and Secrets @@ -81,6 +83,8 @@ jobs: run: | kubectl logs sidecar > /tmp/sidecar.log kubectl logs sidecar-5xx > /tmp/sidecar-5xx.log + kubectl logs sidecar-api-key > /tmp/sidecar-api-key.log + kubectl logs sidecar-api-key-fails > /tmp/sidecar-api-key-fails.log kubectl logs dummy-server-pod > /tmp/dummy-server.log - name: Upload artifacts uses: actions/upload-artifact@v3 @@ -109,9 +113,24 @@ jobs: 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 + echo "Downloading resource files from sidecar-api-key..." + kubectl cp sidecar-api-key:/tmp/api-key.txt /tmp/sidecar-api-key.txt || true + + echo "Downloading resource files from sidecar-api-key-fails..." + kubectl cp sidecar-api-key-fails:/tmp/api-key.txt /tmp/sidecar-api-key-fails.txt || true + - name: DEBUG + run: | + echo "#############################################################" + echo "#############################################################" + echo "#############################################################" + kubectl logs sidecar-api-key + ls -la /tmp/ + echo "#############################################################" + echo "#############################################################" + echo "#############################################################" - name: Verify files run: | - echo "Verifying file content from sidecar and sidecar-5xx ..." + echo "Verifying file content from different sidecar pods ..." # this needs to be the last statement so that it defines the script exit code echo -n "Hello World!" | diff - /tmp/hello.world && diff test/kubelogo.png /tmp/cm-kubelogo.png && @@ -126,4 +145,7 @@ 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 && + grep -i "success" /tmp/sidecar-api-key.txt && echo "api-key used successfully for authentication." && + [ ! -f /tmp/sidecar-api-key-fails.txt ] && echo "api-key fails expectedly with wrong credentials." + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7d33cdb5..f819e6e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +*.iml +.idea/ +venv +.vscode/ # Created by https://www.toptal.com/developers/gitignore/api/python # Edit at https://www.toptal.com/developers/gitignore?templates=python diff --git a/README.md b/README.md index fdf4dcd3..38d3a560 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ metadata: k8s-sidecar-target-directory: "/path/to/target/directory" ``` -If the filename ends with `.url` suffix, the content will be processed as a URL which the target file contents will be downloaded from. +If the filename ends with `.url` suffix, the content will be processed as a URL which the target file contents will be downloaded from. If the source requires authentication, currently http basic-auth and token based authentication via query param are supported. Please see the variables below for details. ## Configuration Environment Variables @@ -69,6 +69,8 @@ If the filename ends with `.url` suffix, the content will be processed as a URL | `REQ_RETRY_READ` | How many times to retry on read errors for any http request (`.url` triggered requests, requests to `REQ_URI` and k8s api requests) | false | `5` | integer | | `REQ_RETRY_BACKOFF_FACTOR` | A backoff factor to apply between attempts after the second try for any http request (`.url` triggered requests, requests to `REQ_URI` and k8s api requests) | false | `1.1` | float | | `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_TOKEN_KEY` | Key of the key/value pair passed as parameter within the url for token based authentication for requests to `REQ_URL` and for `*.url` triggered requests. If `REQ_TOKEN_VALUE` is set and `REQ_TOKEN_KEY` is not, it defaults to "`private_token`". | false | - | string | +| `REQ_TOKEN_VALUE` | Value of the key/value pair passed as parameter within the url for token based authentication for requests to `REQ_URL` and for `*.url` triggered requests. If configured it takes precedence over eventually also configured basic-auth credentials (`REQ_USERNAME`/`REQ_PASSWORD`) | false | - | string | | `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 | | `SCRIPT` | Absolute path to shell script to execute after a configmap got reloaded. It runs before calls to `REQ_URI` | false | - | string | diff --git a/example.yaml b/example.yaml index 278c7828..560c492f 100644 --- a/example.yaml +++ b/example.yaml @@ -28,6 +28,10 @@ spec: volumeMounts: - name: shared-volume mountPath: /tmp/ + # Only relevant for token based auth (like with git) +# envFrom: +# - secretRef: +# name: token-auth-sample-secret env: - name: LABEL value: "findme" @@ -71,6 +75,18 @@ data: # base64 encoded: my super cool \n multiline \ secret secret.world: bXkgc3VwZXIgY29vbAptdWx0aWxpbmUKc2VjcmV0 --- +# This secret is only necessary for token based authentication of http requests. +# Refer to README.md for details on the two parameters REQ_TOKEN_KEY and REQ_TOKEN_VALUE +apiVersion: v1 +kind: Secret +metadata: + name: token-auth-sample-secret +type: Opaque +data: + #REQ_TOKEN_KEY: private_token + # base64 encoded super-duper-secret authentication token + REQ_TOKEN_VALUE: c3VwZXItZHVwZXItc2VjcmV0 +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: diff --git a/src/helpers.py b/src/helpers.py index 4155dbf3..ab41a2e4 100755 --- a/src/helpers.py +++ b/src/helpers.py @@ -100,12 +100,28 @@ def remove_file(folder, filename): def request(url, method, enable_5xx=False, payload=None): enforce_status_codes = list() if enable_5xx else [500, 502, 503, 504] - username = os.getenv("REQ_USERNAME") - password = os.getenv("REQ_PASSWORD") - if username and password: + username = {} if os.getenv("REQ_USERNAME") is None else os.getenv("REQ_USERNAME") + password = {} if os.getenv("REQ_PASSWORD") is None else os.getenv("REQ_PASSWORD") + req_token_key = {} if os.getenv("REQ_TOKEN_KEY") is None else os.getenv("REQ_TOKEN_KEY") + req_token_value = {} if os.getenv("REQ_TOKEN_VALUE") is None else os.getenv("REQ_TOKEN_VALUE") + params=dict() + + if req_token_value and not req_token_key: + req_token_key = "private_token" + logger.info(f"Request token value is set, but key is not. Setting request token key to default value {req_token_key}.") + + if req_token_key and req_token_value: + auth = None + params[ str(req_token_key) ] = req_token_value + logger.debug(f"Token based authorization configured. Token key: {req_token_key}.") + elif username and password: auth = (username, password) + params = None + logger.debug(f"Basic-auth configured. username: {username}.") else: + logger.debug(f"No authorization tokens set (no basic-auth and no private token).") auth = None + params = None r = requests.Session() @@ -123,9 +139,9 @@ def request(url, method, enable_5xx=False, payload=None): # If method is not provided use GET as default if method == "GET" or not method: - res = r.get("%s" % url, auth=auth, timeout=REQ_TIMEOUT) + res = r.get("%s" % url, auth=auth, params=params, timeout=REQ_TIMEOUT) elif method == "POST": - res = r.post("%s" % url, auth=auth, json=payload, timeout=REQ_TIMEOUT) + res = r.post("%s" % url, auth=auth, params=params, json=payload, timeout=REQ_TIMEOUT) else: logger.warning(f"Invalid REQ_METHOD: '{method}', please use 'GET' or 'POST'. Doing nothing.") return diff --git a/test/resources/resources.yaml b/test/resources/resources.yaml index ee621ccf..608804f4 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-api-key + labels: + findme: "sure" +data: + api-key.txt.url: "http://dummy-server/200/api-key.txt" \ No newline at end of file diff --git a/test/resources/sidecar.yaml b/test/resources/sidecar.yaml index f2f75430..602fdcfd 100644 --- a/test/resources/sidecar.yaml +++ b/test/resources/sidecar.yaml @@ -105,6 +105,80 @@ spec: defaultMode: 0777 --- apiVersion: v1 +kind: Secret +metadata: + name: token-auth-sample-secret +type: Opaque +data: + REQ_TOKEN_KEY: cHJpdmF0ZV90b2tlbg== + #REQ_TOKEN_VALUE: base64 enc "super-duper-secret" + REQ_TOKEN_VALUE: c3VwZXItZHVwZXItc2VjcmV0 +--- +apiVersion: v1 +kind: Pod +metadata: + name: sidecar-api-key + namespace: default +spec: + serviceAccountName: sample-acc + containers: + - name: sidecar + image: kiwigrid/k8s-sidecar:testing + volumeMounts: + - name: shared-volume + mountPath: /tmp/ + envFrom: + - secretRef: + name: token-auth-sample-secret + env: + - name: LABEL + value: "findme" + - name: FOLDER + value: /tmp/ + - name: RESOURCE + value: configmap + volumes: + - name: shared-volume + emptyDir: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: token-auth-sample-secret-fails +type: Opaque +data: + REQ_TOKEN_KEY: cHJpdmF0ZV90b2tlbg== + #REQ_TOKEN_VALUE base64 enc "I-will-fail" + REQ_TOKEN_VALUE: SS13aWxsLWZhaWw= +--- +apiVersion: v1 +kind: Pod +metadata: + name: sidecar-api-key-fails + namespace: default +spec: + serviceAccountName: sample-acc + containers: + - name: sidecar + image: kiwigrid/k8s-sidecar:testing + volumeMounts: + - name: shared-volume + mountPath: /tmp/ + envFrom: + - secretRef: + name: token-auth-sample-secret-fails + env: + - name: LABEL + value: "findme" + - name: FOLDER + value: /tmp/ + - name: RESOURCE + value: configmap + volumes: + - name: shared-volume + emptyDir: {} +--- +apiVersion: v1 kind: Pod metadata: name: dummy-server-pod diff --git a/test/server/Dockerfile b/test/server/Dockerfile index 1992415a..b1f9e8f5 100644 --- a/test/server/Dockerfile +++ b/test/server/Dockerfile @@ -2,4 +2,5 @@ FROM python:3.9-alpine RUN pip install fastapi uvicorn EXPOSE 80 COPY server.py /server.py +COPY api-key.txt /api-key.txt CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/test/server/api-key.txt b/test/server/api-key.txt new file mode 100644 index 00000000..caaa5e19 --- /dev/null +++ b/test/server/api-key.txt @@ -0,0 +1 @@ +Success! I can only be downloaded with the correct combination of API Key and password! \ No newline at end of file diff --git a/test/server/server.py b/test/server/server.py index 29f198e6..60903f5f 100644 --- a/test/server/server.py +++ b/test/server/server.py @@ -1,8 +1,19 @@ -from fastapi import FastAPI -import uvicorn +from fastapi import FastAPI, Security, Depends, HTTPException +from fastapi.security.api_key import APIKeyQuery, APIKey +from fastapi.responses import FileResponse + + +API_KEY_NAME="private_token" +API_KEY="super-duper-secret" +api_key_query = APIKeyQuery(name=API_KEY_NAME, auto_error=True) app = FastAPI() +def get_api_key (api_key_query: str = Security(api_key_query)): + if api_key_query == API_KEY: + return api_key_query + else: + raise HTTPException(403) @app.get("/", status_code=200) def read_root(): @@ -27,3 +38,8 @@ async def read_item(): @app.post("/503", status_code=503) async def read_item(): return 503 + + +@app.get("/200/api-key.txt", response_class=FileResponse) +def read_root(api_key: APIKey = Depends(get_api_key)): + return "api-key.txt"