Authenticate cross-Verse asset sources
This commit is contained in:
parent
bdc942db21
commit
12637367e1
5 changed files with 79 additions and 10 deletions
|
|
@ -44,6 +44,10 @@ transport resolves that exact asset session and commits assets as one generation
|
|||
catalog, verified bundles, leases, lookups, render policy, and surface lowering
|
||||
are prepared off to the side, then swapped together. A failed or superseded
|
||||
candidate is disposed without unloading or rewriting the mounted generation.
|
||||
The mounted provider's trust policy authenticates only that provider. A
|
||||
provider-qualified asset source in another Verse/runtime uses the configured
|
||||
cross-target Odin trust for its catalog, subscription, manifests, content, and
|
||||
bundles; local-development trust does not cross that boundary.
|
||||
|
||||
Navigation keeps the outgoing presentation quiesced until the candidate route
|
||||
is ready, then commits the provider route before activating the candidate.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ namespace GameCult.Eve.UnityScene
|
|||
private readonly string _surfaceId;
|
||||
private readonly string _runtimeId;
|
||||
private readonly CultMeshAuthorityTrustPolicy _authorityTrust;
|
||||
private readonly CultMeshAuthorityTrustPolicy _crossTargetAuthorityTrust;
|
||||
private readonly Dictionary<string, EveSurfaceCommandRequest> _pendingCommands =
|
||||
new Dictionary<string, EveSurfaceCommandRequest>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Task<ReceiptSubscription>> _receiptSubscriptions =
|
||||
|
|
@ -83,7 +84,8 @@ namespace GameCult.Eve.UnityScene
|
|||
string surfaceId,
|
||||
string runtimeId = "eve-unity",
|
||||
CultMeshBodyPublicationResolver? bodyResolver = null,
|
||||
CultMeshAuthorityTrustPolicy? authorityTrust = null)
|
||||
CultMeshAuthorityTrustPolicy? authorityTrust = null,
|
||||
CultMeshAuthorityTrustPolicy? crossTargetAuthorityTrust = null)
|
||||
{
|
||||
_cachePath = string.IsNullOrWhiteSpace(cachePath)
|
||||
? throw new ArgumentException("Cache path must be non-empty.", nameof(cachePath))
|
||||
|
|
@ -107,6 +109,7 @@ namespace GameCult.Eve.UnityScene
|
|||
_bodyResolver = bodyResolver;
|
||||
_authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy(
|
||||
CultMeshAuthorityTrustMode.AuthenticatedRemote);
|
||||
_crossTargetAuthorityTrust = crossTargetAuthorityTrust ?? _authorityTrust;
|
||||
CurrentSurfaceDocument = EmptySurfaceDocument();
|
||||
CurrentAssetManifestDocument = EmptyAssetManifest();
|
||||
CurrentInputCapability = new EveInputCapabilityDocument();
|
||||
|
|
@ -493,16 +496,21 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
if (_meshClient != null)
|
||||
return;
|
||||
_meshClient = CreateMeshClient(new[] { _rendezvousEndpoint });
|
||||
_meshClient = CreateMeshClient(new[] { _rendezvousEndpoint }, _authorityTrust);
|
||||
}
|
||||
|
||||
private CultMeshClient CreateMeshClient(IReadOnlyList<string> rendezvousEndpoints)
|
||||
private CultMeshClient CreateMeshClient(
|
||||
IReadOnlyList<string> rendezvousEndpoints,
|
||||
CultMeshAuthorityTrustPolicy authorityTrust)
|
||||
{
|
||||
return new CultMeshClient(new CultMeshClientOptions
|
||||
{
|
||||
RendezvousEndpoints = rendezvousEndpoints,
|
||||
Discovery = EveUnityCultMeshConnectivity.Discovery(),
|
||||
Sessions = new CultMeshSessionManagerOptions { Trust = _authorityTrust },
|
||||
Sessions = new CultMeshSessionManagerOptions
|
||||
{
|
||||
Trust = authorityTrust ?? throw new ArgumentNullException(nameof(authorityTrust))
|
||||
},
|
||||
Connectors = EveUnityCultMeshConnectivity.SchemaConnectors(),
|
||||
ContentConnectors = EveUnityCultMeshConnectivity.ContentConnectors(),
|
||||
RealtimeConnectors = new ICultMeshRealtimeTransportConnector[]
|
||||
|
|
@ -1338,9 +1346,13 @@ namespace GameCult.Eve.UnityScene
|
|||
if (string.Equals(source.Identity, CurrentAssetSourceIdentity, StringComparison.Ordinal))
|
||||
return await AssetMeshClient()
|
||||
.ReadAsync<EveAssetCatalogDocument>(source.Target, source.ManifestRecordRef, cancellationToken);
|
||||
using var candidateClient = source.RendezvousEndpoints.Count == 0
|
||||
? null
|
||||
: CreateMeshClient(source.RendezvousEndpoints);
|
||||
var crossTarget = IsCrossTarget(source);
|
||||
var endpoints = AssetRendezvousEndpoints(source, crossTarget);
|
||||
using var candidateClient = crossTarget || source.RendezvousEndpoints.Count > 0
|
||||
? CreateMeshClient(
|
||||
endpoints,
|
||||
AssetAuthorityTrust(source))
|
||||
: null;
|
||||
return await (candidateClient ?? _meshClient ??
|
||||
throw new InvalidOperationException("The CultMesh client is not open."))
|
||||
.ReadAsync<EveAssetCatalogDocument>(source.Target, source.ManifestRecordRef, cancellationToken);
|
||||
|
|
@ -1351,11 +1363,17 @@ namespace GameCult.Eve.UnityScene
|
|||
EveAssetCatalogDocument catalog,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var crossTarget = IsCrossTarget(source);
|
||||
var endpoints = AssetRendezvousEndpoints(source, crossTarget);
|
||||
var candidate = new AssetGeneration(
|
||||
source,
|
||||
source.Identity,
|
||||
catalog.Version,
|
||||
source.RendezvousEndpoints.Count > 0 ? CreateMeshClient(source.RendezvousEndpoints) : null);
|
||||
crossTarget || source.RendezvousEndpoints.Count > 0
|
||||
? CreateMeshClient(
|
||||
endpoints,
|
||||
AssetAuthorityTrust(source))
|
||||
: null);
|
||||
try
|
||||
{
|
||||
ConfigureAssetCatalog(candidate, catalog);
|
||||
|
|
@ -1371,6 +1389,24 @@ namespace GameCult.Eve.UnityScene
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsCrossTarget(AssetSource source) =>
|
||||
!string.Equals(source.Target.VerseId, _target.VerseId, StringComparison.Ordinal) ||
|
||||
!string.Equals(source.Target.AuthorityRuntimeId, _target.AuthorityRuntimeId, StringComparison.Ordinal);
|
||||
|
||||
private CultMeshAuthorityTrustPolicy AssetAuthorityTrust(AssetSource source) =>
|
||||
IsCrossTarget(source) ? _crossTargetAuthorityTrust : _authorityTrust;
|
||||
|
||||
private IReadOnlyList<string> AssetRendezvousEndpoints(AssetSource source, bool crossTarget)
|
||||
{
|
||||
if (source.RendezvousEndpoints.Count > 0)
|
||||
return source.RendezvousEndpoints;
|
||||
if (crossTarget)
|
||||
throw new InvalidOperationException(
|
||||
$"Cross-target asset source '{source.Target.VerseId}/{source.Target.AuthorityRuntimeId}' " +
|
||||
"must advertise at least one Odin rendezvous endpoint.");
|
||||
return new[] { _rendezvousEndpoint };
|
||||
}
|
||||
|
||||
private async Task AttachAssetCatalogSubscriptionAsync(
|
||||
AssetGeneration candidate,
|
||||
CancellationToken cancellationToken)
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ namespace GameCult.Eve.UnityScene
|
|||
target.VerseId,
|
||||
target.AuthorityRuntimeId,
|
||||
_navigationAuthorityTrust,
|
||||
_navigationAuthorityTrust,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
StagePrepared(prepared);
|
||||
|
|
@ -355,6 +356,7 @@ namespace GameCult.Eve.UnityScene
|
|||
verseFilter,
|
||||
"",
|
||||
_authorityTrust,
|
||||
_navigationAuthorityTrust,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
TraceStartup($"discovery and transport preparation {elapsed.Elapsed.TotalMilliseconds:0.###}ms");
|
||||
AdoptPrepared(prepared);
|
||||
|
|
@ -368,6 +370,7 @@ namespace GameCult.Eve.UnityScene
|
|||
string requiredVerseId,
|
||||
string requiredAuthorityRuntimeId,
|
||||
CultMeshAuthorityTrustPolicy trust,
|
||||
CultMeshAuthorityTrustPolicy crossTargetTrust,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
|
|
@ -395,7 +398,8 @@ namespace GameCult.Eve.UnityScene
|
|||
selection.ProviderId,
|
||||
selection.SurfaceId,
|
||||
runtimeId,
|
||||
authorityTrust: trust);
|
||||
authorityTrust: trust,
|
||||
crossTargetAuthorityTrust: crossTargetTrust);
|
||||
try
|
||||
{
|
||||
await transport.PrepareAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
|||
|
|
@ -526,6 +526,31 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
UnityEngine.Object.DestroyImmediate(prefab);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CrossTargetAssetsUseConfiguredRemoteTrust()
|
||||
{
|
||||
var localTrust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.LocalDevelopment);
|
||||
var remoteTrust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.AuthenticatedRemote);
|
||||
using var transport = new EveUnityCultMeshLiveProviderTransport(
|
||||
Path.Combine(Path.GetTempPath(), $"eve-asset-trust-{Guid.NewGuid():N}.cc"),
|
||||
"cultnet+tcp://127.0.0.1:1",
|
||||
"verse-a",
|
||||
"authority-a",
|
||||
"provider-a",
|
||||
"surface-a",
|
||||
authorityTrust: localTrust,
|
||||
crossTargetAuthorityTrust: remoteTrust);
|
||||
var trustMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod(
|
||||
"AssetAuthorityTrust",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
var localSource = CreateAssetSource("verse-a", "authority-a", "provider-a", "catalog-a");
|
||||
var remoteSource = CreateAssetSource("verse-b", "authority-b", "provider-b", "catalog-b");
|
||||
|
||||
Assert.That(trustMethod.Invoke(transport, new[] { localSource }), Is.SameAs(localTrust));
|
||||
Assert.That(trustMethod.Invoke(transport, new[] { remoteSource }), Is.SameAs(remoteTrust));
|
||||
}
|
||||
|
||||
private static object CreateAssetSource(
|
||||
string verseId,
|
||||
string authorityRuntimeId,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.126",
|
||||
"version": "0.3.127",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
|
|
|
|||
Reference in a new issue