feat(deploy): provision Unscouted smoke tenant (#22)
This commit is contained in:
@@ -32,6 +32,16 @@
|
|||||||
"NotBefore": "2026-01-01T00:00:00Z",
|
"NotBefore": "2026-01-01T00:00:00Z",
|
||||||
"SignUntil": "2100-01-01T00:00:00Z",
|
"SignUntil": "2100-01-01T00:00:00Z",
|
||||||
"VerifyUntil": "2100-01-02T00:00:00Z"
|
"VerifyUntil": "2100-01-02T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"KeyId": "local-smoke-unscouted-1",
|
||||||
|
"SecretReference": "file:/run/secrets/rendezvous-signing-key",
|
||||||
|
"CredentialKinds": ["DedicatedPublisher"],
|
||||||
|
"GameId": "unscouted",
|
||||||
|
"EnvironmentId": "smoke",
|
||||||
|
"NotBefore": "2026-01-01T00:00:00Z",
|
||||||
|
"SignUntil": "2100-01-01T00:00:00Z",
|
||||||
|
"VerifyUntil": "2100-01-02T00:00:00Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Games": [
|
"Games": [
|
||||||
@@ -53,6 +63,27 @@
|
|||||||
"MaxAnonymousListingsPerAddress": 0,
|
"MaxAnonymousListingsPerAddress": 0,
|
||||||
"MaxActiveJoinAttempts": 100,
|
"MaxActiveJoinAttempts": 100,
|
||||||
"FallbackPolicy": "DedicatedEndpointAllowed"
|
"FallbackPolicy": "DedicatedEndpointAllowed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GameId": "unscouted",
|
||||||
|
"EnvironmentId": "smoke",
|
||||||
|
"Enabled": true,
|
||||||
|
"ProtocolVersions": [1],
|
||||||
|
"Regions": ["local"],
|
||||||
|
"VisibilityModes": ["Public"],
|
||||||
|
"PublisherTrustModes": ["ManagedDedicated"],
|
||||||
|
"MetadataValueMaxBytes": {
|
||||||
|
"mode": 32,
|
||||||
|
"world": 64,
|
||||||
|
"mods": 64
|
||||||
|
},
|
||||||
|
"RequiredMetadataKeys": ["mode", "world", "mods"],
|
||||||
|
"MetadataMaxBytes": 512,
|
||||||
|
"MetadataMaxKeys": 3,
|
||||||
|
"MaxListingsPerPrincipal": 10,
|
||||||
|
"MaxAnonymousListingsPerAddress": 0,
|
||||||
|
"MaxActiveJoinAttempts": 100,
|
||||||
|
"FallbackPolicy": "DedicatedEndpointAllowed"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,25 @@ set -euo pipefail
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}"
|
LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}"
|
||||||
|
GAME_ID="${RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID:-space-game}"
|
||||||
|
|
||||||
|
case "$GAME_ID" in
|
||||||
|
space-game)
|
||||||
|
KEY_ID="local-smoke-1"
|
||||||
|
SUBJECT="local-smoke-host"
|
||||||
|
;;
|
||||||
|
unscouted)
|
||||||
|
KEY_ID="local-smoke-unscouted-1"
|
||||||
|
SUBJECT="local-smoke-unscouted-host"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted.\n' >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if (( $# != 0 )); then
|
if (( $# != 0 )); then
|
||||||
printf 'This helper accepts no arguments and mints only the fixed local Compose smoke scope.\n' >&2
|
printf 'This helper accepts no arguments; select only a provisioned local game through RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID.\n' >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -17,7 +33,7 @@ command -v python3 >/dev/null || {
|
|||||||
# This is deliberately a local-fixture tool, not a general credential issuer.
|
# This is deliberately a local-fixture tool, not a general credential issuer.
|
||||||
# Python reads the raw key from the protected file; key material never appears in
|
# Python reads the raw key from the protected file; key material never appears in
|
||||||
# a child process argument, environment value, temporary file, or command output.
|
# a child process argument, environment value, temporary file, or command output.
|
||||||
python3 - "$LOCAL_KEY" <<'PY'
|
python3 - "$LOCAL_KEY" "$GAME_ID" "$KEY_ID" "$SUBJECT" <<'PY'
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
@@ -29,6 +45,9 @@ import sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
key_path = sys.argv[1]
|
key_path = sys.argv[1]
|
||||||
|
game_id = sys.argv[2]
|
||||||
|
key_id = sys.argv[3]
|
||||||
|
subject = sys.argv[4]
|
||||||
try:
|
try:
|
||||||
metadata = os.lstat(key_path)
|
metadata = os.lstat(key_path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -55,9 +74,9 @@ payload = {
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"issuer": "final-factory-rendezvous-smoke",
|
"issuer": "final-factory-rendezvous-smoke",
|
||||||
"audience": "rendezvous-service",
|
"audience": "rendezvous-service",
|
||||||
"subject": "local-smoke-host",
|
"subject": subject,
|
||||||
"kind": "dedicatedPublisher",
|
"kind": "dedicatedPublisher",
|
||||||
"gameId": "space-game",
|
"gameId": game_id,
|
||||||
"environmentId": "smoke",
|
"environmentId": "smoke",
|
||||||
"regions": ["local"],
|
"regions": ["local"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
@@ -71,7 +90,7 @@ def base64url(value: bytes) -> str:
|
|||||||
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
|
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
|
||||||
|
|
||||||
encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
||||||
signed = f"rv1.local-smoke-1.{encoded}"
|
signed = f"rv1.{key_id}.{encoded}"
|
||||||
signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest())
|
signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest())
|
||||||
print(f"{signed}.{signature}")
|
print(f"{signed}.{signature}")
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -238,6 +238,10 @@ public sealed class ProductionProcessTests
|
|||||||
"--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"),
|
"--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"),
|
||||||
"--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"),
|
"--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"),
|
||||||
"--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"),
|
"--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"),
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:SecretReference", $"file:{secretPath}",
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:NotBefore", now.AddHours(-1).ToString("O"),
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:SignUntil", now.AddHours(1).ToString("O"),
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:VerifyUntil", now.AddHours(2).ToString("O"),
|
||||||
"--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
"--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
"--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
"--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,14 +40,19 @@ public sealed partial class DocumentationContractTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LocalCredentialHelperIsFixedScopeAndSmokeDelegatesToIt()
|
public void LocalCredentialHelperWhitelistsProvisionedGameScopesAndSmokeDelegatesToIt()
|
||||||
{
|
{
|
||||||
string root = FindRepositoryRoot();
|
string root = FindRepositoryRoot();
|
||||||
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
|
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
|
||||||
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
|
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
|
||||||
|
|
||||||
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
|
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("\"gameId\": \"space-game\"", helper, StringComparison.Ordinal);
|
Assert.Contains("space-game)", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("unscouted)", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("KEY_ID=\"local-smoke-1\"", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("KEY_ID=\"local-smoke-unscouted-1\"", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("\"gameId\": game_id", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
|
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
|
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("now + 600", helper, StringComparison.Ordinal);
|
Assert.Contains("now + 600", helper, StringComparison.Ordinal);
|
||||||
@@ -60,6 +65,32 @@ public sealed partial class DocumentationContractTests
|
|||||||
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
|
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnscoutedComposeTenantIsGameScopedAndMetadataBounded()
|
||||||
|
{
|
||||||
|
string root = FindRepositoryRoot();
|
||||||
|
using JsonDocument settings = JsonDocument.Parse(File.ReadAllText(
|
||||||
|
Path.Combine(root, "deploy", "compose", "appsettings.Production.json")));
|
||||||
|
JsonElement provisioning = settings.RootElement.GetProperty("Rendezvous").GetProperty("Provisioning");
|
||||||
|
JsonElement game = provisioning.GetProperty("Games").EnumerateArray().Single(
|
||||||
|
static item => item.GetProperty("GameId").GetString() == "unscouted");
|
||||||
|
JsonElement key = provisioning.GetProperty("SigningKeys").EnumerateArray().Single(
|
||||||
|
static item => item.GetProperty("KeyId").GetString() == "local-smoke-unscouted-1");
|
||||||
|
|
||||||
|
Assert.Equal("smoke", game.GetProperty("EnvironmentId").GetString());
|
||||||
|
Assert.Equal([1], game.GetProperty("ProtocolVersions").EnumerateArray().Select(static value => value.GetInt32()));
|
||||||
|
Assert.Equal(["mode", "mods", "world"], game.GetProperty("MetadataValueMaxBytes")
|
||||||
|
.EnumerateObject().Select(static property => property.Name).Order(StringComparer.Ordinal));
|
||||||
|
Assert.Equal(["mode", "mods", "world"], game.GetProperty("RequiredMetadataKeys")
|
||||||
|
.EnumerateArray().Select(static value => value.GetString()).Order(StringComparer.Ordinal));
|
||||||
|
Assert.Equal(3, game.GetProperty("MetadataMaxKeys").GetInt32());
|
||||||
|
Assert.Equal("DedicatedEndpointAllowed", game.GetProperty("FallbackPolicy").GetString());
|
||||||
|
Assert.Equal("unscouted", key.GetProperty("GameId").GetString());
|
||||||
|
Assert.Equal("smoke", key.GetProperty("EnvironmentId").GetString());
|
||||||
|
Assert.Equal(["DedicatedPublisher"], key.GetProperty("CredentialKinds")
|
||||||
|
.EnumerateArray().Select(static value => value.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
|
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user