Skip to content
Open
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
7 changes: 7 additions & 0 deletions airborne_server/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Superposition configuration
SUPERPOSITION_URL=http://localhost:8080
SUPERPOSITION_RC_URL=http://localhost:8080
SUPERPOSITION_ORG_ID=get-org-id-from-superposition
ENABLE_AUTHENTICATED_SUPERPOSITION=false
SUPERPOSITION_TOKEN=
SUPERPOSITION_USER_TOKEN=
SUPERPOSITION_ORG_TOKEN=
SUPERPOSITION_RC_USER_TOKEN=
SUPERPOSITION_RC_ORG_TOKEN=

# Keycloak settings
KEYCLOAK_URL=http://localhost:8180
Expand Down
6 changes: 6 additions & 0 deletions airborne_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ The server relies on a set of environment variables for its configuration. These
- `KEYCLOAK_REALM`: Keycloak realm name.
- `KEYCLOAK_PUBLIC_KEY`: Public key for validating JWTs issued by Keycloak.
- `SUPERPOSITION_URL`: URL of the Superposition service.
- `SUPERPOSITION_RC_URL`: URL of the Superposition service used by RC (`/release`) endpoints. Defaults to `SUPERPOSITION_URL` when unset.
- `ENABLE_AUTHENTICATED_SUPERPOSITION`: Enables cookie-based auth for Superposition SDK requests.
- `SUPERPOSITION_USER_TOKEN`: User token for Superposition auth cookie (`user=...`).
- `SUPERPOSITION_ORG_TOKEN`: Org token for Superposition auth cookie (`org_<SUPERPOSITION_ORG_ID>=...`).
- `SUPERPOSITION_RC_USER_TOKEN`: RC-specific user token for auth cookie; defaults to `SUPERPOSITION_USER_TOKEN` when unset.
- `SUPERPOSITION_RC_ORG_TOKEN`: RC-specific org token for auth cookie; defaults to `SUPERPOSITION_ORG_TOKEN` when unset.
- `SUPERPOSITION_ORG_ID`: The organization ID within Superposition used by the server.
- `AWS_BUCKET`: Name of the S3 bucket for storing package assets.
- `PUBLIC_ENDPOINT`: The public-facing URL for accessing assets stored in S3.
Expand Down
86 changes: 86 additions & 0 deletions airborne_server/scripts/encrypt-envs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,87 @@ shell_quote() {
printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
}

strip_shell_quotes() {
local value="$1"

if [[ ${#value} -ge 2 ]]; then
if [[ "${value#\'}" != "$value" ]] && [[ "${value%\'}" != "$value" ]]; then
value="${value#\'}"
value="${value%\'}"
value="${value//\'\\\'\'/\'}"
elif [[ "${value#\"}" != "$value" ]] && [[ "${value%\"}" != "$value" ]]; then
value="${value#\"}"
value="${value%\"}"
fi
fi

printf '%s' "$value"
}

read_env_raw() {
local key="$1"
local value
value=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | head -1)
strip_shell_quotes "$value"
}

is_value_empty() {
local value="$1"
case "$value" in
""|"''"|'""')
return 0
;;
*)
return 1
;;
esac
}

upsert_env_raw() {
local key="$1"
local raw_value="$2"
local tmp_file

tmp_file=$(mktemp "${TMPDIR:-/tmp}/airborne-env.XXXXXX")

if [[ -f "$ENV_FILE" ]]; then
awk -v key="$key" -v value="$raw_value" '
BEGIN { updated = 0 }
index($0, key "=") == 1 { print key "=" value; updated = 1; next }
{ print }
END { if (!updated) print key "=" value }
' "$ENV_FILE" > "$tmp_file"
else
printf "%s=%s\n" "$key" "$raw_value" > "$tmp_file"
fi

mv "$tmp_file" "$ENV_FILE"
}

sync_superposition_rc_env_defaults() {
local superposition_url superposition_rc_url
local superposition_user_token superposition_rc_user_token
local superposition_org_token superposition_rc_org_token

superposition_url=$(read_env_raw "SUPERPOSITION_URL")
superposition_rc_url=$(read_env_raw "SUPERPOSITION_RC_URL")
if is_value_empty "$superposition_rc_url"; then
upsert_env_raw "SUPERPOSITION_RC_URL" "$superposition_url"
fi

superposition_user_token=$(read_env_raw "SUPERPOSITION_USER_TOKEN")
superposition_rc_user_token=$(read_env_raw "SUPERPOSITION_RC_USER_TOKEN")
if is_value_empty "$superposition_rc_user_token"; then
upsert_env_raw "SUPERPOSITION_RC_USER_TOKEN" "$superposition_user_token"
fi

superposition_org_token=$(read_env_raw "SUPERPOSITION_ORG_TOKEN")
superposition_rc_org_token=$(read_env_raw "SUPERPOSITION_RC_ORG_TOKEN")
if is_value_empty "$superposition_rc_org_token"; then
upsert_env_raw "SUPERPOSITION_RC_ORG_TOKEN" "$superposition_org_token"
fi
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Function to encrypt a value using AES-GCM
encrypt_value() {
local value="$1"
Expand Down Expand Up @@ -105,6 +186,8 @@ SECRETS=(
"SUPERPOSITION_TOKEN"
"SUPERPOSITION_USER_TOKEN"
"SUPERPOSITION_ORG_TOKEN"
"SUPERPOSITION_RC_USER_TOKEN"
"SUPERPOSITION_RC_ORG_TOKEN"
"GOOGLE_SERVICE_ACCOUNT_KEY"
)

Expand All @@ -123,6 +206,9 @@ if [[ ! -f "$ENV_FILE" ]]; then
cp "$ENV_EXAMPLE" "$ENV_FILE"
fi

# Keep local RC Superposition vars aligned with existing Superposition vars when unset.
sync_superposition_rc_env_defaults

if [[ "$PLAINTEXT_MODE" == true ]]; then
echo -e "${GREEN}✅ Plaintext .env file ready at: $ENV_FILE${NC}"
echo ""
Expand Down
61 changes: 60 additions & 1 deletion airborne_server/scripts/init-localstack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,56 @@ upsert_env_var() {
mv "$tmp_file" "$file"
}

read_env_value() {
local key="$1"
local value=""

if [ -f ".env" ]; then
value=$(grep "^${key}=" ".env" 2>/dev/null | cut -d'=' -f2- | head -1)
value=$(strip_shell_quotes "$value")
fi

echo "$value"
}

is_value_empty() {
local value="$1"
case "$value" in
""|"''"|'""')
return 0
;;
*)
return 1
;;
esac
}

sync_superposition_rc_env_defaults() {
local superposition_url superposition_rc_url
local superposition_user_token superposition_rc_user_token
local superposition_org_token superposition_rc_org_token

superposition_url=$(read_env_value "SUPERPOSITION_URL")
superposition_rc_url=$(read_env_value "SUPERPOSITION_RC_URL")
if is_value_empty "$superposition_rc_url"; then
upsert_env_var ".env" "SUPERPOSITION_RC_URL" "$superposition_url"
fi

superposition_user_token=$(read_env_value "SUPERPOSITION_USER_TOKEN")
superposition_rc_user_token=$(read_env_value "SUPERPOSITION_RC_USER_TOKEN")
if is_value_empty "$superposition_rc_user_token"; then
upsert_env_var ".env" "SUPERPOSITION_RC_USER_TOKEN" "$superposition_user_token"
fi

superposition_org_token=$(read_env_value "SUPERPOSITION_ORG_TOKEN")
superposition_rc_org_token=$(read_env_value "SUPERPOSITION_RC_ORG_TOKEN")
if is_value_empty "$superposition_rc_org_token"; then
upsert_env_var ".env" "SUPERPOSITION_RC_ORG_TOKEN" "$superposition_org_token"
fi
}

sync_superposition_rc_env_defaults
Comment thread
coderabbitai[bot] marked this conversation as resolved.

echo "${YELLOW}☁️ AWS Endpoint:${NC} ${GREEN}${AWS_ENDPOINT_URL}${NC}"
echo "${YELLOW}🪣 S3 Bucket:${NC} ${GREEN}${AWS_BUCKET}${NC}"
echo "${YELLOW}🌍 Region:${NC} ${GREEN}${AWS_REGION}${NC}"
Expand Down Expand Up @@ -162,7 +212,16 @@ aws --endpoint-url=${AWS_ENDPOINT_URL} s3 mb s3://$AWS_BUCKET >/dev/null 2>&1 ||
echo "${GREEN}✅ S3 bucket ready: $AWS_BUCKET${NC}"

# Variables that need encryption/processing
SENSITIVE_VARS=("DB_PASSWORD" "DB_MIGRATION_PASSWORD" "KEYCLOAK_SECRET")
SENSITIVE_VARS=(
"DB_PASSWORD"
"DB_MIGRATION_PASSWORD"
"KEYCLOAK_SECRET"
"SUPERPOSITION_TOKEN"
"SUPERPOSITION_USER_TOKEN"
"SUPERPOSITION_ORG_TOKEN"
"SUPERPOSITION_RC_USER_TOKEN"
"SUPERPOSITION_RC_ORG_TOKEN"
)

# Get values from .env.example or .env.generated
get_value() {
Expand Down
25 changes: 21 additions & 4 deletions airborne_server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ pub struct AppConfig {

// Superposition settings
pub superposition_url: String,
pub superposition_rc_url: String,
pub superposition_org_id: String,
pub superposition_token: Option<String>,
pub superposition_user_token: Option<String>,
pub superposition_org_token: Option<String>,
pub superposition_rc_user_token: Option<String>,
pub superposition_rc_org_token: Option<String>,
pub enable_authenticated_superposition: bool,

// Feature flags
Expand Down Expand Up @@ -135,6 +138,17 @@ impl AppConfig {
let get_optional =
|name: &str| -> Option<String> { env::var(name).ok().filter(|v| !v.is_empty()) };

let superposition_url = get_env("SUPERPOSITION_URL", None)?;
let superposition_rc_url =
get_optional("SUPERPOSITION_RC_URL").unwrap_or_else(|| superposition_url.clone());
let superposition_token = get_optional_secret("SUPERPOSITION_TOKEN")?;
let superposition_user_token = get_optional_secret("SUPERPOSITION_USER_TOKEN")?;
let superposition_org_token = get_optional_secret("SUPERPOSITION_ORG_TOKEN")?;
let superposition_rc_user_token = get_optional_secret("SUPERPOSITION_RC_USER_TOKEN")?
.or_else(|| superposition_user_token.clone());
let superposition_rc_org_token = get_optional_secret("SUPERPOSITION_RC_ORG_TOKEN")?
.or_else(|| superposition_org_token.clone());

Ok(AppConfig {
// Server settings
port: parse_env("PORT", 8081),
Expand Down Expand Up @@ -168,11 +182,14 @@ impl AppConfig {
keycloak_public_key: get_env("KEYCLOAK_PUBLIC_KEY", None)?,

// Superposition settings
superposition_url: get_env("SUPERPOSITION_URL", None)?,
superposition_url,
superposition_rc_url,
superposition_org_id: get_env("SUPERPOSITION_ORG_ID", None)?,
superposition_token: get_optional_secret("SUPERPOSITION_TOKEN")?,
superposition_user_token: get_optional_secret("SUPERPOSITION_USER_TOKEN")?,
superposition_org_token: get_optional_secret("SUPERPOSITION_ORG_TOKEN")?,
superposition_token,
superposition_user_token,
superposition_org_token,
superposition_rc_user_token,
superposition_rc_org_token,
enable_authenticated_superposition: parse_env(
"ENABLE_AUTHENTICATED_SUPERPOSITION",
false,
Expand Down
94 changes: 62 additions & 32 deletions airborne_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ async fn main() -> std::io::Result<()> {
let secret = app_config.keycloak_secret.clone();
let superposition_token = app_config.superposition_token.clone().unwrap_or_default();

let cac_url = app_config.superposition_url.clone();
let dashboard_superposition_url = app_config.superposition_url.clone();
let rc_superposition_url = app_config.superposition_rc_url.clone();
let superposition_org_id_env = app_config.superposition_org_id.clone();

let env = types::Environment {
Expand Down Expand Up @@ -202,44 +203,73 @@ async fn main() -> std::io::Result<()> {
hub = Some(Sheets::new(client, gcp_auth));
}

let superposition_client = if app_config.enable_authenticated_superposition {
let superposition_user_token = app_config.superposition_user_token.clone().expect(
"SUPERPOSITION_USER_TOKEN must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
);
let superposition_org_token = app_config.superposition_org_token.clone().expect(
"SUPERPOSITION_ORG_TOKEN must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
);

// Inject Auth cookie for Superposition SDK calls
let cookie_interceptor = CookieIntercept::new(format!(
"user={}; org_{}={}",
superposition_user_token, superposition_org_id_env, superposition_org_token,
));

superposition_sdk::Client::from_conf(
SrsConfig::builder()
.endpoint_url(cac_url.clone())
.behavior_version_latest()
.bearer_token(superposition_token.into())
.interceptor(cookie_interceptor)
.build(),
)
} else {
superposition_sdk::Client::from_conf(
SrsConfig::builder()
.endpoint_url(cac_url.clone())
.behavior_version_latest()
.bearer_token(superposition_token.into())
.build(),
)
};
let create_superposition_client =
|endpoint_url: String,
user_token: Option<String>,
org_token: Option<String>,
user_token_env_hint: &str,
org_token_env_hint: &str| {
if app_config.enable_authenticated_superposition {
let superposition_user_token = user_token.unwrap_or_else(|| {
panic!(
"{} must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
user_token_env_hint
)
});
let superposition_org_token = org_token.unwrap_or_else(|| {
panic!(
"{} must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
org_token_env_hint
)
});

// Inject Auth cookie for Superposition SDK calls
let cookie_interceptor = CookieIntercept::new(format!(
"user={}; org_{}={}",
superposition_user_token, superposition_org_id_env, superposition_org_token,
));

superposition_sdk::Client::from_conf(
SrsConfig::builder()
.endpoint_url(endpoint_url)
.behavior_version_latest()
.bearer_token(superposition_token.clone().into())
.interceptor(cookie_interceptor)
.build(),
)
} else {
superposition_sdk::Client::from_conf(
SrsConfig::builder()
.endpoint_url(endpoint_url)
.behavior_version_latest()
.bearer_token(superposition_token.clone().into())
.build(),
)
}
};

let superposition_client = create_superposition_client(
dashboard_superposition_url,
app_config.superposition_user_token.clone(),
app_config.superposition_org_token.clone(),
"SUPERPOSITION_USER_TOKEN",
"SUPERPOSITION_ORG_TOKEN",
);
let rc_superposition_client = create_superposition_client(
rc_superposition_url,
app_config.superposition_rc_user_token.clone(),
app_config.superposition_rc_org_token.clone(),
"SUPERPOSITION_RC_USER_TOKEN or SUPERPOSITION_USER_TOKEN",
"SUPERPOSITION_RC_ORG_TOKEN or SUPERPOSITION_ORG_TOKEN",
);

let app_state = Arc::new(types::AppState {
env: env.clone(),
db_pool: pool,
s3_client: aws_s3_client,
cf_client: aws_cloudfront_client,
superposition_client,
rc_superposition_client,
sheets_hub: hub,
});

Expand Down
Loading
Loading