Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ You can then use the baton command in your workflow.

The sync-test action tests syncing, granting, and revoking for a baton connector.

Before the grant/revoke cycle it also syncs once with deliberately invalid credentials and asserts the connector exits with gRPC `Unauthenticated` (16) or `PermissionDenied` (7). That mapping requires `exit.LogExit` from baton-sdk v0.25.0+; connectors that still `os.Exit(1)` skip this check. Dummy values default to `invalid` for credential-like `BATON_*` env vars (tokens, passwords, secrets, keys). Override with `bad-credentials` when the value must parse as JWT, PEM, JSON, or similar before the connector will attempt authentication.

```yaml
- name: Test Connector Sync
uses: ConductorOne/github-workflows/actions/sync-test@v4
Expand All @@ -265,6 +267,9 @@ The sync-test action tests syncing, granting, and revoking for a baton connector
baton-principal: "user123"
baton-principal-type: "user"
sleep: 2 # optional, wait 2 seconds after each write operation
# optional, only needed when dummy credentials must be well-formed:
# bad-credentials: |
# BATON_PRIVATE_KEY=-----BEGIN PRIVATE KEY----- invalid -----END PRIVATE KEY-----
```

### Account Provisioning Test
Expand Down
20 changes: 20 additions & 0 deletions actions/sync-test/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,33 @@ inputs:
Example:
required-secrets: BATON_NETLIFY_ACCESS_TOKEN
required: false
bad-credentials:
description: |
Env-file of invalid credentials for the unauthenticated sync check.
Each line is NAME=value. When omitted, credential-like BATON_* env vars
(and names listed in required-secrets) are set to "invalid". Override
when dummy credentials must be well-formed (JWT, PEM, JSON key, etc.).

Example:
bad-credentials: |
BATON_TOKEN=invalid
BATON_PRIVATE_KEY=-----BEGIN PRIVATE KEY----- invalid -----END PRIVATE KEY-----
required: false

runs:
using: "composite"
steps:
- name: Download Baton
uses: ConductorOne/github-workflows/actions/get-baton@v4

- name: Auth Error
env:
BATON_CONNECTOR: ${{ inputs.connector }}
REQUIRED_SECRETS: ${{ inputs.required-secrets }}
BAD_CREDENTIALS: ${{ inputs.bad-credentials }}
run: ${{ github.action_path }}/auth-error.sh
shell: bash

- name: Grant Revoke
env:
BATON_CONNECTOR: ${{ inputs.connector }}
Expand Down
150 changes: 150 additions & 0 deletions actions/sync-test/auth-error.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/bin/bash

# Sync with deliberately invalid credentials and assert the connector exits
# with a gRPC status code: Unauthenticated (16) or PermissionDenied (7).
#
# Connectors must call exit.LogExit (baton-sdk v0.25.0+) to surface those
# codes. The older main.go pattern always os.Exit(1); treat that as "not
# migrated yet" and skip rather than fail.

set -euo pipefail

GRPC_UNAUTHENTICATED=16
GRPC_PERMISSION_DENIED=7
DEFAULT_INVALID_CREDENTIAL="invalid"

if [ -z "${BATON_CONNECTOR:-}" ]; then
echo "BATON_CONNECTOR not set"
exit 1
fi

# Returns 0 if NAME looks like a connector credential env var.
# Only BATON_* names are considered here; required-secrets and
# bad-credentials can name additional vars.
is_credential_var() {
local name="$1"
case "$name" in
BATON_CONNECTOR | BATON_ENTITLEMENT | BATON_PRINCIPAL | BATON_PRINCIPAL_TYPE | BATON)
return 1
;;
esac
case "$name" in
BATON_*) ;;
*) return 1 ;;
esac
# Connection/config, not a secret.
case "$name" in
*_URL | *_ENDPOINT | *_PATH | *_HOST | *_ADDRESS | *_TIMEOUT | *_LEVEL | *_FILE | *_DIR)
return 1
;;
esac
# Identifiers that often sit next to secrets.
case "$name" in
*_KEY_ID | *_CLIENT_ID | *_TENANT_ID | *_ORGANIZATION_ID | *_ORG_ID)
return 1
;;
esac
case "$name" in
*TOKEN* | *PASSWORD* | *SECRET* | *KEY* | *CREDENTIAL* | *PAT | *PASSWD* | *BEARER*)
return 0
;;
esac
return 1
}

# Collect unique env var names to overwrite, preserving insertion order.
names=()
seen=" "

add_name() {
local name="$1"
case "$seen" in
*" $name "*) return 0 ;;
esac
seen+="$name "
names+=("$name")
}

while IFS= read -r name; do
[ -z "$name" ] && continue
if is_credential_var "$name"; then
add_name "$name"
fi
done < <(compgen -e | LC_ALL=C sort)

if [ -n "${REQUIRED_SECRETS:-}" ]; then
for name in ${REQUIRED_SECRETS//,/ }; do
[ -z "$name" ] && continue
add_name "$name"
done
fi

if [ ${#names[@]} -eq 0 ] && [ -z "${BAD_CREDENTIALS:-}" ]; then
echo "::notice title=auth-error test skipped::no credential env vars found to invalidate; skipping unauthenticated sync test"
exit 0
fi

if [ ${#names[@]} -gt 0 ]; then
for name in "${names[@]}"; do
export "${name}=${DEFAULT_INVALID_CREDENTIAL}"
done
fi

# Overlay caller-supplied dummy credentials. Used when a value must parse
# as JWT, PEM, JSON, etc. before the connector will attempt authentication.
if [ -n "${BAD_CREDENTIALS:-}" ]; then
while IFS= read -r line || [ -n "$line" ]; do
line="${line%$'\r'}"
# trim leading whitespace
line="${line#"${line%%[![:space:]]*}"}"
[ -z "$line" ] && continue
[ "${line:0:1}" = "#" ] && continue
case "$line" in
*=*) ;;
*)
echo "bad-credentials line is not NAME=value: $line"
exit 1
;;
esac
name="${line%%=*}"
value="${line#*=}"
if [ -z "$name" ]; then
echo "bad-credentials line is missing NAME: $line"
exit 1
fi
export "${name}=${value}"
add_name "$name"
done <<< "$BAD_CREDENTIALS"
fi

if [ ${#names[@]} -eq 0 ]; then
echo "::notice title=auth-error test skipped::no credential env vars found to invalidate; skipping unauthenticated sync test"
exit 0
fi

echo "Syncing with invalid credentials in: ${names[*]}"

set +e
"$BATON_CONNECTOR"
exit_code=$?
set -e

case "$exit_code" in
"$GRPC_UNAUTHENTICATED" | "$GRPC_PERMISSION_DENIED")
echo "Connector exited $exit_code as expected for invalid credentials."
exit 0
;;
1)
echo "::notice title=auth-error test skipped::connector exited 1; unauthenticated sync test requires exit.LogExit (baton-sdk v0.25.0+). Skipping until the connector is updated."
exit 0
;;
0)
echo "Connector succeeded (exit 0) with invalid credentials; expected Unauthenticated (16) or PermissionDenied (7)."
exit 1
;;
*)
echo "Connector exited $exit_code with invalid credentials; expected Unauthenticated (16) or PermissionDenied (7)."
echo "If dummy credentials failed config validation, set the bad-credentials input to well-formed invalid values."
exit 1
;;
esac