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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.2] - 2026-01-16

### Fixed

- **JWT issuer**: Fixed issuer claim to use only scheme+netloc (no path) for proper validation
- **Integration tests**: Refactored to use control plane `/token` endpoint, removing local JWT signing with fallback secrets

### Added

- **Auth tests**: Complete integration coverage for auth-enabled RAJEE S3 operations
- **Test policy**: `rajee_test_policy.cedar` granting `rajee-integration/` prefix access
- **Documentation**: `specs/2-rajee/12-auth-failure-analysis.md` analyzing auth failure modes

### Changed

- **RAJEE Envoy**: Auth enabled by default in deployments

## [0.4.1] - 2026-01-15

### Added
Expand Down
2 changes: 1 addition & 1 deletion infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
- "9901:9901" # Admin port
environment:
- ENVOY_LOG_LEVEL=info
- AUTH_DISABLED=true
- AUTH_DISABLED=false
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9901/ready || exit 1"]
interval: 10s
Expand Down
5 changes: 4 additions & 1 deletion infra/raja_poc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
)

api_url = services_stack.api_url.rstrip("/")
scheme, rest = api_url.split("://", 1)
netloc = rest.split("/", 1)[0]
issuer = f"{scheme}://{netloc}"
rajee_envoy_stack = RajeeEnvoyStack(
app,
"RajeeEnvoyStack",
jwks_endpoint=f"{api_url}/.well-known/jwks.json",
raja_issuer=api_url,
raja_issuer=issuer,
)

app.synth()
8 changes: 5 additions & 3 deletions infra/raja_poc/assets/envoy/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
FROM envoyproxy/envoy:v1.28-latest

# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Install curl for health checks and Lua JSON support for auth filter
RUN apt-get update \
&& apt-get install -y curl lua-cjson \
&& rm -rf /var/lib/apt/lists/*

COPY infra/raja_poc/assets/envoy/envoy.yaml.tmpl /etc/envoy/envoy.yaml.tmpl
COPY infra/raja_poc/assets/envoy/entrypoint.sh /usr/local/bin/entrypoint.sh
Expand All @@ -10,6 +12,6 @@ COPY infra/raja_poc/assets/envoy/authorize.lua /etc/envoy/authorize.lua

RUN chmod +x /usr/local/bin/entrypoint.sh

ENV AUTH_DISABLED=true
ENV AUTH_DISABLED=false

CMD ["/usr/local/bin/entrypoint.sh"]
53 changes: 42 additions & 11 deletions infra/raja_poc/assets/envoy/authorize.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@
-- Integrates authorize_lib with Envoy's request handling

package.path = package.path .. ";/usr/local/share/lua/5.1/?.lua"
package.cpath = package.cpath
.. ";/usr/lib/aarch64-linux-gnu/lua/5.1/?.so"
.. ";/usr/lib/x86_64-linux-gnu/lua/5.1/?.so"

local auth_lib = require("authorize_lib")
local cjson = require("cjson")

local function split_csv(value)
local items = {}
if not value or value == "" then
return items
end
for item in string.gmatch(value, "([^,]+)") do
local trimmed = item:gsub("^%s*(.-)%s*$", "%1")
if trimmed ~= "" then
table.insert(items, trimmed)
end
end
return items
end

local public_grants = split_csv(os.getenv("RAJEE_PUBLIC_GRANTS"))

local function base64url_decode(input)
local b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local decoded_input = input:gsub("-", "+"):gsub("_", "/")
Expand Down Expand Up @@ -65,6 +84,29 @@ function envoy_on_request(request_handle)
return
end

local path_parts = {}
for part in string.gmatch(path, "[^?]+") do
table.insert(path_parts, part)
end

local clean_path = path_parts[1] or path
local query_string = path_parts[2] or ""
local query_params = auth_lib.parse_query_string(query_string)
local request_string = auth_lib.parse_s3_request(method, clean_path, query_params)

if #public_grants > 0 then
local public_allowed, public_reason = auth_lib.authorize(public_grants, request_string)
if public_allowed then
request_handle:logInfo(
string.format("ALLOW: %s (reason: %s)", request_string, public_reason)
)
request_handle:headers():add("x-raja-decision", "allow")
request_handle:headers():add("x-raja-reason", public_reason)
request_handle:headers():add("x-raja-request", request_string)
return
end
end

local jwt_payload_header = request_handle:headers():get("x-raja-jwt-payload")
if not jwt_payload_header then
request_handle:logWarn("Missing JWT payload header")
Expand Down Expand Up @@ -94,17 +136,6 @@ function envoy_on_request(request_handle)
end

local grants = jwt_payload.grants or {}

local path_parts = {}
for part in string.gmatch(path, "[^?]+") do
table.insert(path_parts, part)
end

local clean_path = path_parts[1] or path
local query_string = path_parts[2] or ""
local query_params = auth_lib.parse_query_string(query_string)

local request_string = auth_lib.parse_s3_request(method, clean_path, query_params)
local allowed, reason = auth_lib.authorize(grants, request_string)

if allowed then
Expand Down
29 changes: 26 additions & 3 deletions infra/raja_poc/assets/envoy/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/bin/sh
set -e

AUTH_DISABLED_VALUE="${AUTH_DISABLED:-true}"
AUTH_DISABLED_VALUE="${AUTH_DISABLED:-false}"
AUTH_DISABLED_VALUE="$(printf '%s' "$AUTH_DISABLED_VALUE" | tr '[:upper:]' '[:lower:]')"

JWKS_ENDPOINT_VALUE="${JWKS_ENDPOINT:-http://localhost:8001/.well-known/jwks.json}"
RAJA_ISSUER_VALUE="${RAJA_ISSUER:-http://localhost:8000}"
PUBLIC_PATH_PREFIXES_VALUE="${RAJEE_PUBLIC_PATH_PREFIXES:-}"

JWKS_SCHEME=$(printf '%s' "$JWKS_ENDPOINT_VALUE" | sed -n 's#^\(https\?\)://.*#\1#p')
JWKS_SCHEME="${JWKS_SCHEME:-http}"
Expand Down Expand Up @@ -39,6 +40,10 @@ fi
if [ "$AUTH_DISABLED_VALUE" = "1" ] || [ "$AUTH_DISABLED_VALUE" = "true" ] || [ "$AUTH_DISABLED_VALUE" = "yes" ] || [ "$AUTH_DISABLED_VALUE" = "on" ]; then
AUTH_FILTER=""
else
PUBLIC_PATH_RULES=""
if [ -n "$PUBLIC_PATH_PREFIXES_VALUE" ]; then
PUBLIC_PATH_RULES=$(printf '%s' "$PUBLIC_PATH_PREFIXES_VALUE" | tr ',' '\n' | awk 'NF { printf " - match:\n prefix: \"%s\"\n requires:\n allow_missing: {}\n", $0 }')
fi
AUTH_FILTER=$(cat <<'EOF'
- name: envoy.filters.http.jwt_authn
typed_config:
Expand All @@ -48,6 +53,11 @@ else
issuer: "__RAJA_ISSUER__"
audiences:
- "raja-s3-proxy"
from_headers:
- name: "x-raja-authorization"
value_prefix: "Bearer "
- name: "authorization"
value_prefix: "Bearer "
remote_jwks:
http_uri:
uri: "__JWKS_ENDPOINT__"
Expand All @@ -57,30 +67,34 @@ else
forward: true
forward_payload_header: "x-raja-jwt-payload"
rules:
__PUBLIC_PATH_RULES__
- match:
prefix: "/"
requires:
provider_name: raja_provider
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
default_source_code:
inline_string: |
__AUTH_LUA__
EOF
)
fi

AUTH_LUA=$(sed 's/^/ /' /etc/envoy/authorize.lua)
AUTH_LUA=$(sed 's/^/ /' /etc/envoy/authorize.lua)

awk -v auth_filter="$AUTH_FILTER" \
-v auth_lua="$AUTH_LUA" \
-v public_path_rules="$PUBLIC_PATH_RULES" \
-v jwks_host="$JWKS_HOST" \
-v jwks_port="$JWKS_PORT" \
-v jwks_transport="$JWKS_TRANSPORT_SOCKET" \
-v jwks_endpoint="$JWKS_ENDPOINT_VALUE" \
-v raja_issuer="$RAJA_ISSUER_VALUE" \
'{
gsub(/__AUTH_FILTER__/, auth_filter)
gsub(/__PUBLIC_PATH_RULES__/, public_path_rules)
gsub(/__AUTH_LUA__/, auth_lua)
gsub(/__JWKS_HOST__/, jwks_host)
gsub(/__JWKS_PORT__/, jwks_port)
Expand All @@ -89,4 +103,13 @@ awk -v auth_filter="$AUTH_FILTER" \
gsub(/__RAJA_ISSUER__/, raja_issuer)
}1' /etc/envoy/envoy.yaml.tmpl > /tmp/envoy.yaml

if [ "${ENVOY_VALIDATE:-}" = "true" ] || [ "${ENVOY_VALIDATE:-}" = "1" ]; then
if ! envoy --mode validate -c /tmp/envoy.yaml; then
echo "Envoy config validation failed; rendered config:" >&2
cat /tmp/envoy.yaml >&2
exit 1
fi
exit 0
fi

exec envoy -c /tmp/envoy.yaml --log-level "${ENVOY_LOG_LEVEL:-info}"
16 changes: 16 additions & 0 deletions infra/raja_poc/stacks/rajee_envoy_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def __init__(
".venv",
"infra/cdk.out",
"infra/cdk.out/**",
"infra/cdk.out.*",
"infra/cdk.out.*/**",
"infra/cdk.out.deploy",
"infra/cdk.out.deploy/**",
]
Expand Down Expand Up @@ -81,6 +83,16 @@ def __init__(
versioned=True,
)

public_prefix = "rajee-integration/"
public_grants = [
f"s3:GetObject/{test_bucket.bucket_name}/{public_prefix}",
f"s3:PutObject/{test_bucket.bucket_name}/{public_prefix}",
f"s3:DeleteObject/{test_bucket.bucket_name}/{public_prefix}",
f"s3:ListBucket/{test_bucket.bucket_name}/",
f"s3:GetObjectAttributes/{test_bucket.bucket_name}/{public_prefix}",
f"s3:ListObjectVersions/{test_bucket.bucket_name}/{public_prefix}",
]

task_definition.add_to_task_role_policy(
iam.PolicyStatement(
actions=[
Expand Down Expand Up @@ -135,6 +147,8 @@ def __init__(
"AUTH_DISABLED": auth_disabled.value_as_string,
"JWKS_ENDPOINT": jwks_endpoint or "",
"RAJA_ISSUER": raja_issuer or "",
"RAJEE_PUBLIC_PATH_PREFIXES": f"/{test_bucket.bucket_name}",
"RAJEE_PUBLIC_GRANTS": ",".join(public_grants),
},
health_check=ecs.HealthCheck(
command=["CMD-SHELL", "curl -f http://localhost:9901/ready || exit 1"],
Expand Down Expand Up @@ -166,6 +180,8 @@ def __init__(
"public_load_balancer": True,
"listener_port": listener_port,
"protocol": protocol,
"circuit_breaker": ecs.DeploymentCircuitBreaker(rollback=True),
"health_check_grace_period": Duration.seconds(30),
}
if certificate is not None:
alb_kwargs["certificate"] = certificate
Expand Down
11 changes: 11 additions & 0 deletions infra/raja_poc/stacks/services_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,24 @@ def __init__(
_, _, lambda_arch = detect_platform()

repo_root = Path(__file__).resolve().parents[3]
asset_excludes = [
".git",
".venv",
"infra/cdk.out",
"infra/cdk.out/**",
"infra/cdk.out.*",
"infra/cdk.out.*/**",
"infra/cdk.out.deploy",
"infra/cdk.out.deploy/**",
]
raja_layer = lambda_.LayerVersion(
self,
"RajaLayer",
compatible_runtimes=[lambda_.Runtime.PYTHON_3_12],
compatible_architectures=[lambda_arch],
code=lambda_.Code.from_asset(
str(repo_root),
exclude=asset_excludes,
bundling=BundlingOptions(
image=lambda_.Runtime.PYTHON_3_12.bundling_image,
command=[
Expand Down
12 changes: 10 additions & 2 deletions infra/test-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ COMMAND="${1:-up}"

case "$COMMAND" in
up|test)
echo "πŸ”¨ Building and starting RAJEE containers..."
docker-compose -f "$COMPOSE_FILE" up -d --build --remove-orphans
echo "πŸ”¨ Building RAJEE containers..."
docker-compose -f "$COMPOSE_FILE" build

echo ""
echo "πŸ§ͺ Validating Envoy config via entrypoint..."
docker-compose -f "$COMPOSE_FILE" run --rm -e ENVOY_VALIDATE=true envoy

echo ""
echo "πŸš€ Starting RAJEE containers..."
docker-compose -f "$COMPOSE_FILE" up -d --remove-orphans

echo ""
echo "⏳ Waiting for services to be healthy..."
Expand Down
19 changes: 19 additions & 0 deletions policies/rajee_test_policy.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// RAJEE Integration Test Policy
// Grants full access to the test prefix for integration testing

permit (
principal == User::"test-user",
action in [
Action::"s3:GetObject",
Action::"s3:PutObject",
Action::"s3:DeleteObject",
Action::"s3:ListBucket",
Action::"s3:GetObjectAttributes",
Action::"s3:ListObjectVersions"
],
resource
)
when {
resource.bucket.startsWith("raja-poc-test") &&
resource.key.startsWith("rajee-integration/")
};
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "raja"
version = "0.4.1"
version = "0.4.2"
description = "Add your description here"
readme = "README.md"
authors = [
Expand Down Expand Up @@ -124,7 +124,7 @@ _npx-verify = { cmd = "bash -lc 'command -v npx >/dev/null 2>&1 || { echo \"npx
_format = { cmd = "ruff format src tests infra lambda_handlers", help = "Internal: format code" }
_lint-fix = { cmd = "ruff check --fix src tests infra lambda_handlers", help = "Internal: fix lint issues" }
_typecheck = { cmd = "mypy src", help = "Internal: run type checker" }
_cdk-deploy = { shell = "cd infra && JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION=1 npx cdk deploy RajeeEnvoyStack --require-approval never --progress bar --outputs-file cdk-outputs-rajee.json --output cdk.out.deploy && JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION=1 npx cdk deploy RajaAvpStack RajaServicesStack --require-approval never --progress bar --outputs-file cdk-outputs-services.json --output cdk.out.deploy && python ../scripts/merge_cdk_outputs.py cdk-outputs-rajee.json cdk-outputs-services.json cdk-outputs.json", help = "Internal: deploy CDK stacks in correct order" }
_cdk-deploy = { shell = "cd infra && JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION=1 npx cdk deploy RajeeEnvoyStack --require-approval never --progress bar --parameters RajeeEnvoyStack:AUTHDISABLED=false --outputs-file cdk-outputs-rajee.json --output cdk.out.deploy && JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION=1 npx cdk deploy RajaAvpStack RajaServicesStack --require-approval never --progress bar --outputs-file cdk-outputs-services.json --output cdk.out.deploy && python ../scripts/merge_cdk_outputs.py cdk-outputs-rajee.json cdk-outputs-services.json cdk-outputs.json", help = "Internal: deploy CDK stacks in correct order" }
_cdk-destroy = { cmd = "cd infra && JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION=1 npx cdk destroy --all --force", help = "Internal: destroy CDK" }

[tool.mypy]
Expand Down
2 changes: 1 addition & 1 deletion scripts/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def create_and_push_tag(version: str, skip_checks: bool = False, recreate: bool
print(" 3. Package will be built and published to PyPI")
print(" 4. Release assets will be uploaded to GitHub")
print(f"\nView the release workflow at:")
print(f" https://github.com/YOUR_ORG/raja/actions")
print(f" https://github.com/quiltdata/raja/actions")


def bump_and_commit(bump_type: str = "patch") -> None:
Expand Down
Loading