|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting.DevTunnels;
internal interface IDevTunnelClient
{
Task<UserLoginStatus> GetUserLoginStatusAsync(CancellationToken cancellationToken = default);
Task<UserLoginStatus> UserLoginAsync(LoginProvider provider, CancellationToken cancellationToken = default);
Task<DevTunnelStatus> CreateOrUpdateTunnelAsync(string tunnelId, DevTunnelOptions options, CancellationToken cancellationToken = default);
Task<DevTunnelPortStatus> CreateOrUpdatePortAsync(string tunnelId, int portNumber, DevTunnelPortOptions options, CancellationToken cancellationToken = default);
Task<DevTunnelStatus> GetTunnelAsync(string tunnelId, CancellationToken cancellationToken = default);
Task<DevTunnelAccessStatus> GetAccessAsync(string tunnelId, int? portNumber = null, CancellationToken cancellationToken = default);
}
internal sealed record UserLoginStatus(string Status, LoginProvider Provider, string Username)
{
public bool IsLoggedIn => string.Equals(Status, "Logged in", StringComparison.OrdinalIgnoreCase);
}
internal enum LoginProvider
{
Microsoft,
GitHub
}
internal sealed record DevTunnelStatus(string TunnelId, int HostConnections, int ClientConnections, string Description, IReadOnlyList<string> Labels)
{
public IReadOnlyList<DevTunnelPort> Ports { get; init; } = [];
public sealed record DevTunnelPort(int PortNumber, string Protocol)
{
public Uri? PortUri { get; init; }
}
}
internal sealed record DevTunnelPortStatus(string TunnelId, int PortNumber, string Protocol, int ClientConnections);
internal sealed record DevTunnelAccessStatus
{
public IReadOnlyList<AccessControlEntry> AccessControlEntries { get; init; } = [];
public sealed record AccessControlEntry(string Type, bool IsDeny, bool IsInherited, IReadOnlyList<string> Subjects, IReadOnlyList<string> Scopes);
internal string LogAnonymousAccessPolicy(ILogger logger)
{
const string AnonymousType = "Anonymous";
const string ConnectScope = "connect";
static bool HasConnectScope(AccessControlEntry entry) => entry.Scopes is { } scopes && scopes.Any(s => string.Equals(s, ConnectScope, StringComparison.OrdinalIgnoreCase));
var entries = AccessControlEntries;
var portHasInheritedAnonymousAllow = entries.Any(e => string.Equals(e.Type, AnonymousType, StringComparison.OrdinalIgnoreCase)
&& !e.IsDeny
&& e.IsInherited
&& HasConnectScope(e));
var portHasExplicitAnonymousAllow = entries.Any(e => string.Equals(e.Type, AnonymousType, StringComparison.OrdinalIgnoreCase)
&& !e.IsDeny
&& !e.IsInherited
&& HasConnectScope(e));
var portHasExplicitAnonymousDeny = entries.Any(e => string.Equals(e.Type, AnonymousType, StringComparison.OrdinalIgnoreCase)
&& e.IsDeny
&& HasConnectScope(e));
// Derive tunnel-level allow from presence of inherited allow (since we don't receive tunnel access status directly here)
var tunnelHasAnonymousAllow = portHasInheritedAnonymousAllow;
string effective;
if (tunnelHasAnonymousAllow && portHasInheritedAnonymousAllow && !portHasExplicitAnonymousDeny && !portHasExplicitAnonymousAllow)
{
// Case 1: tunnel allows anonymous; port inherits allow; no deny override
logger.LogInformation("!! Anonymous access is allowed (inherited from tunnel) !!");
effective = "Allowed";
}
else if (tunnelHasAnonymousAllow && portHasExplicitAnonymousDeny)
{
// Case 2: tunnel allows anonymous but port explicitly denies
logger.LogInformation("Anonymous access is not allowed (tunnel allows it but port explicitly denies it)");
effective = "Denied";
}
else if (!tunnelHasAnonymousAllow && portHasExplicitAnonymousAllow && !portHasExplicitAnonymousDeny)
{
// Case 3: tunnel does not allow but port explicitly allows
logger.LogInformation("!! Anonymous access is allowed (port explicitly allows it) !!");
effective = "Allowed";
}
else if (!tunnelHasAnonymousAllow && portHasExplicitAnonymousDeny)
{
// Case 4: tunnel does not allow and port explicitly denies
logger.LogInformation("Anonymous access is not allowed (tunnel does not allow it and port explicitly denies it)");
effective = "Denied";
}
else if (tunnelHasAnonymousAllow && portHasExplicitAnonymousAllow && !portHasExplicitAnonymousDeny)
{
// Case 5: tunnel allows anonymous; port allows anonymous; no deny override
logger.LogInformation("!! Anonymous access is allowed (tunnel allows it and port allows it) !!");
effective = "Allowed";
}
else if (!tunnelHasAnonymousAllow && !portHasExplicitAnonymousAllow && !portHasExplicitAnonymousDeny)
{
// Case 6: tunnel does not allow; port does not explicitly allow or deny
logger.LogInformation("Anonymous access is not allowed (tunnel does not allow it and port does not explicitly allow or deny it)");
effective = "Denied";
}
else
{
// Fallback / other combinations
effective = "Unknown";
logger.LogDebug("Anonymous access: TunnelAllow={TunnelAllow} InheritedAllow={InheritedAllow} ExplicitAllow={ExplicitAllow} ExplicitDeny={ExplicitDeny} Effective={Effective}",
tunnelHasAnonymousAllow, portHasInheritedAnonymousAllow, portHasExplicitAnonymousAllow, portHasExplicitAnonymousDeny, effective);
}
return effective;
}
}
|