mirror of
https://github.com/flectar/mail.git
synced 2026-11-14 02:53:32 +00:00
chore: validate Android OAuth configuration
This commit is contained in:
1 parent
2ee87d1e07
commit
ddf2ce63b3
9 files changed
+451
-44
No files matched your search
@@ -29,14 +29,7 @@ if [[ ! -f "$FLECTAR_ANDROID_KEYSTORE" ]]; then
|
||||
printf 'Production keystore not found: %s\n' "$FLECTAR_ANDROID_KEYSTORE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$FLECTAR_GOOGLE_ANDROID_CLIENT_ID" != *.apps.googleusercontent.com || "$FLECTAR_GOOGLE_ANDROID_CLIENT_ID" == test.* ]]; then
|
||||
printf 'FLECTAR_GOOGLE_ANDROID_CLIENT_ID is not a production Android OAuth client ID.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$FLECTAR_MICROSOFT_ANDROID_CLIENT_ID" =~ ^[0-9A-Fa-f-]{36}$ || "$FLECTAR_MICROSOFT_ANDROID_CLIENT_ID" == 00000000-0000-0000-0000-000000000000 ]]; then
|
||||
printf 'FLECTAR_MICROSOFT_ANDROID_CLIENT_ID is not a production Entra application ID.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
python3 "$project_dir/scripts/verify-android-oauth.py" --require-all
|
||||
if [[ ! "$FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI" =~ ^msauth://com\.flectar\.mail/.+ ]]; then
|
||||
printf 'Microsoft redirect must be msauth://com.flectar.mail/<signature-hash>.\n' >&2
|
||||
exit 1
|
||||
|
||||
@@ -6,6 +6,10 @@ target_dir="$project_dir/target"
|
||||
android_sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
|
||||
android_ndk="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-}}"
|
||||
|
||||
# Missing registrations are supported: the app shows Sign-in settings instead
|
||||
# of sending a placeholder client ID to a provider. Reject invalid supplied IDs.
|
||||
python3 "$project_dir/scripts/verify-android-oauth.py"
|
||||
|
||||
if [[ -z "$android_sdk" || ! -d "$android_sdk" ]]; then
|
||||
printf 'Android SDK not found. Set ANDROID_HOME to your Android SDK directory.\n' >&2
|
||||
exit 1
|
||||
@@ -68,14 +72,6 @@ fi
|
||||
export ANDROID_HOME="$android_sdk"
|
||||
export CARGO_TARGET_DIR="$target_dir"
|
||||
|
||||
# Test builds have deterministic provider placeholders so static packaging and
|
||||
# device startup can be exercised without borrowing production registrations.
|
||||
# Provider sign-in intentionally remains unavailable until the release-owner
|
||||
# values documented in ANDROID_FUNCTIONAL_APP_TODO.md are supplied.
|
||||
export FLECTAR_GOOGLE_ANDROID_CLIENT_ID="${FLECTAR_GOOGLE_ANDROID_CLIENT_ID:-test.apps.googleusercontent.com}"
|
||||
export FLECTAR_MICROSOFT_ANDROID_CLIENT_ID="${FLECTAR_MICROSOFT_ANDROID_CLIENT_ID:-00000000-0000-0000-0000-000000000000}"
|
||||
export FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI="${FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI:-msauth://com.flectar.mail/test-signature-hash}"
|
||||
|
||||
cargo build \
|
||||
--locked \
|
||||
--manifest-path "$project_dir/platform/android/Cargo.toml" \
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate Android OAuth configuration and the actual build-script environment."""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
SCRIPTS = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location("oauth", SCRIPTS / "verify-android-oauth.py")
|
||||
oauth = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(oauth)
|
||||
GOOGLE = "FLECTAR_GOOGLE_ANDROID_CLIENT_ID"
|
||||
MICROSOFT = "FLECTAR_MICROSOFT_ANDROID_CLIENT_ID"
|
||||
REDIRECT = "FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI"
|
||||
VALID = {
|
||||
GOOGLE: "123456789-fixture.apps.googleusercontent.com",
|
||||
MICROSOFT: "abcdef01-2345-4678-9abc-def012345678",
|
||||
}
|
||||
|
||||
|
||||
class AndroidOAuthTests(unittest.TestCase):
|
||||
def test_optional_providers_and_production_requirements(self):
|
||||
for values in ({}, {GOOGLE: "", MICROSOFT: " "}, {GOOGLE: VALID[GOOGLE]}, {MICROSOFT: VALID[MICROSOFT]}):
|
||||
oauth.verify(values)
|
||||
with self.assertRaises(ValueError):
|
||||
oauth.verify(values, require_all=True)
|
||||
oauth.verify(VALID, require_all=True)
|
||||
oauth.verify({GOOGLE: VALID[GOOGLE], MICROSOFT: " " + VALID[MICROSOFT].upper() + " "})
|
||||
|
||||
def test_rejects_placeholders_and_malformed_ids_without_echoing_values(self):
|
||||
for key, value in (
|
||||
(MICROSOFT, "00000000-0000-0000-0000-000000000000"),
|
||||
(MICROSOFT, "------------------------------------"),
|
||||
(MICROSOFT, "a" * 36),
|
||||
(MICROSOFT, VALID[MICROSOFT] + "?unexpected=value"),
|
||||
(MICROSOFT, "invalid-app-id"),
|
||||
(GOOGLE, "test.apps.googleusercontent.com"),
|
||||
(GOOGLE, "TEST.apps.googleusercontent.com"),
|
||||
(GOOGLE, "wrong-suffix"),
|
||||
):
|
||||
with self.subTest(key=key, value=value):
|
||||
with self.assertRaises(ValueError) as error:
|
||||
oauth.verify({key: value})
|
||||
self.assertIn(key, str(error.exception))
|
||||
self.assertNotIn(value, str(error.exception))
|
||||
|
||||
def run_build(self, script, configuration):
|
||||
# Run the real shell entry points. Replace Cargo with a recorder that
|
||||
# stops before compilation; no SDK downloads, signing or network calls.
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "scripts").mkdir()
|
||||
for name in (script, "verify-android-oauth.py"):
|
||||
shutil.copyfile(SCRIPTS / name, root / "scripts" / name)
|
||||
sdk = root / "sdk"
|
||||
sdk.mkdir()
|
||||
toolchain = root / "ndk/toolchains/llvm/prebuilt/fixture/bin"
|
||||
toolchain.mkdir(parents=True)
|
||||
binaries = root / "bin"
|
||||
binaries.mkdir()
|
||||
for path in (toolchain / "aarch64-linux-android26-clang", binaries / "gradle", binaries / "keytool"):
|
||||
path.write_text("#!/bin/sh\nexit 99\n")
|
||||
path.chmod(0o755)
|
||||
capture = root / "cargo-environment.json"
|
||||
cargo = binaries / "cargo"
|
||||
cargo.write_text(
|
||||
"#!/usr/bin/env python3\nimport os,json,pathlib\n"
|
||||
"pathlib.Path(os.environ['OAUTH_CAPTURE']).write_text(json.dumps({"
|
||||
"key:os.environ.get(key) for key in " + repr((GOOGLE, MICROSOFT, REDIRECT)) + "}))\n"
|
||||
"raise SystemExit(73)\n"
|
||||
)
|
||||
cargo.chmod(0o755)
|
||||
keystore = root / "test.keystore"
|
||||
keystore.touch()
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith("FLECTAR_")}
|
||||
env.update({
|
||||
"PATH": str(binaries) + os.pathsep + env.get("PATH", ""),
|
||||
"ANDROID_HOME": str(sdk),
|
||||
"ANDROID_NDK_ROOT": str(root / "ndk"),
|
||||
"GRADLE_CMD": str(binaries / "gradle"),
|
||||
"XDG_DATA_HOME": str(root / "data"),
|
||||
"FLECTAR_ANDROID_TEST_KEYSTORE": str(keystore),
|
||||
"FLECTAR_ANDROID_KEYSTORE": str(keystore),
|
||||
"FLECTAR_ANDROID_KEYSTORE_PASSWORD": "fixture",
|
||||
"FLECTAR_ANDROID_KEY_ALIAS": "fixture",
|
||||
"FLECTAR_ANDROID_KEY_PASSWORD": "fixture",
|
||||
"FLECTAR_ANDROID_VERSION_CODE": "1",
|
||||
"FLECTAR_ANDROID_VERSION_NAME": "0.1.0",
|
||||
"OAUTH_CAPTURE": str(capture),
|
||||
**configuration,
|
||||
})
|
||||
result = subprocess.run(["bash", str(root / "scripts" / script)],
|
||||
env=env, capture_output=True, text=True, timeout=15)
|
||||
recorded = json.loads(capture.read_text()) if capture.exists() else None
|
||||
return result, recorded
|
||||
|
||||
def test_preview_never_injects_placeholder_defaults(self):
|
||||
for values in ({}, {GOOGLE: "", MICROSOFT: "", REDIRECT: ""}, VALID):
|
||||
with self.subTest(configuration=values):
|
||||
result, recorded = self.run_build("build-android-test-apk.sh", values)
|
||||
self.assertEqual(result.returncode, 73, result.stdout + result.stderr)
|
||||
self.assertEqual(recorded, {key: values.get(key) for key in (GOOGLE, MICROSOFT, REDIRECT)})
|
||||
|
||||
def test_preview_rejects_invalid_ids_before_compilation(self):
|
||||
for values in ({MICROSOFT: "00000000-0000-0000-0000-000000000000"}, {GOOGLE: "test.apps.googleusercontent.com"}):
|
||||
result, recorded = self.run_build("build-android-test-apk.sh", values)
|
||||
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
||||
self.assertIsNone(recorded)
|
||||
self.assertIn("not a valid", result.stderr)
|
||||
|
||||
def test_production_requires_real_ids_before_compilation(self):
|
||||
values = {**VALID, REDIRECT: "msauth://com.flectar.mail/fixture-signature"}
|
||||
result, recorded = self.run_build("build-android-release.sh", values)
|
||||
self.assertEqual(result.returncode, 73, result.stdout + result.stderr)
|
||||
self.assertEqual(recorded, values)
|
||||
for invalid in ("", "00000000-0000-0000-0000-000000000000", "a" * 36):
|
||||
result, recorded = self.run_build("build-android-release.sh", {**values, MICROSOFT: invalid})
|
||||
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
||||
self.assertIsNone(recorded)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reject invalid bundled Android OAuth IDs without printing configuration values.
|
||||
|
||||
Preview APKs may omit either provider. Production builds require both. Local
|
||||
validation cannot establish that an app is registered or that consent is allowed.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def verify(environment, require_all=False):
|
||||
for provider, variable in (
|
||||
("Google", "FLECTAR_GOOGLE_ANDROID_CLIENT_ID"),
|
||||
("Microsoft", "FLECTAR_MICROSOFT_ANDROID_CLIENT_ID"),
|
||||
):
|
||||
value = environment.get(variable, "").strip()
|
||||
if not value:
|
||||
if require_all:
|
||||
raise ValueError(f"Production Android build requires {variable}.")
|
||||
continue
|
||||
if provider == "Microsoft":
|
||||
valid = (
|
||||
re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", value)
|
||||
and value != "00000000-0000-0000-0000-000000000000"
|
||||
)
|
||||
else:
|
||||
valid = (
|
||||
re.fullmatch(r"[^\s]+\.apps\.googleusercontent\.com", value)
|
||||
and not value.lower().startswith("test.")
|
||||
)
|
||||
if not valid:
|
||||
raise ValueError(
|
||||
f"{variable} is not a valid {provider} application ID. "
|
||||
"Use a real app registration, or omit it for a preview without bundled sign-in."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--require-all", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
verify(os.environ, args.require_all)
|
||||
except ValueError as error:
|
||||
print(error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in new issue
Block a user