diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index cbcc125..973bc41 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -10,4 +10,10 @@ services:
POSTGRES_DB: gandalf_reborn
ports:
- "5432:5432"
+ token-cache:
+ image: redis:alpine
+ container_name: token-cache
+ restart: always
+ ports:
+ - "6379:6379"
\ No newline at end of file
diff --git a/src/dotnet/.idea/.idea.Suspectus.Gandalf/.idea/dataSources.xml b/src/dotnet/.idea/.idea.Suspectus.Gandalf/.idea/dataSources.xml
index 2d89c97..015da6c 100644
--- a/src/dotnet/.idea/.idea.Suspectus.Gandalf/.idea/dataSources.xml
+++ b/src/dotnet/.idea/.idea.Suspectus.Gandalf/.idea/dataSources.xml
@@ -9,5 +9,13 @@
jdbc:postgresql://localhost:5432/gandalf_reborn?logServerErrorDetail=True&password=root&user=root
$ProjectFileDir$
+
+ redis
+ true
+ true
+ jdbc.RedisDriver
+ jdbc:redis://localhost:6379/0
+ $ProjectFileDir$
+
\ No newline at end of file
diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs
new file mode 100644
index 0000000..d315d66
--- /dev/null
+++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs
@@ -0,0 +1,109 @@
+using System.Net;
+using System.Net.Http.Headers;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Server.HttpSys;
+using Microsoft.Extensions.Caching.Distributed;
+using Suspectus.Gandalf.Core.Abstractions.Extensions;
+using Suspectus.Gandalf.Palantir.Client;
+
+namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class AppProxyController : ControllerBase
+{
+ private readonly IDistributedCache _tokenCache;
+ private readonly IPalantirClient _client;
+ private readonly HttpClient _httpClient;
+
+ public AppProxyController(IDistributedCache tokenCache, IPalantirClient client, IHttpClientFactory httpClientFactory)
+ {
+ _tokenCache = tokenCache;
+ _client = client;
+ _httpClient = httpClientFactory.CreateClient("test");
+ }
+
+ [HttpGet("{appId}/{*path}")]
+ public async Task Get(string appId, string path, CancellationToken cancellationToken)
+ {
+ var sessionCookie = Request.Cookies["MithrandirSession"];
+
+ if (sessionCookie is null)
+ {
+ return Unauthorized("Session expired.");
+ }
+
+ var appInfoResult = await _client.Internal.App.GetInfo(appId);
+
+ if (!appInfoResult.IsSuccess)
+ {
+ return BadRequest(appInfoResult.Match(_ => null, e => e.Message));
+ }
+
+ var appInfo = appInfoResult.GetValue();
+
+ if (appInfo.IsMaster && path.StartsWith("api/internal"))
+ {
+ return NotFound();
+ }
+
+ var token = await GetToken(sessionCookie, appId, cancellationToken);
+
+ _httpClient.BaseAddress = new Uri(appInfo.BaseAddress);
+ _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
+
+ try
+ {
+ var clientResponse = await _httpClient.GetAsync(path, cancellationToken);
+
+ if (!clientResponse.IsSuccessStatusCode)
+ {
+ Response.StatusCode = (int)clientResponse.StatusCode;
+ }
+
+ foreach (var header in clientResponse.Content.Headers)
+ {
+ Response.Headers[header.Key] = header.Value.ToArray();
+ }
+
+ await Response.WriteAsync(await clientResponse.Content.ReadAsStringAsync(cancellationToken), cancellationToken: cancellationToken);
+ }
+ catch (HttpRequestException e)
+ {
+ Response.StatusCode = e.StatusCode is null ? (int)HttpStatusCode.InternalServerError : (int)e.StatusCode;
+ await Response.WriteAsync(e.Message, cancellationToken: cancellationToken);
+ }
+
+ return new EmptyResult();
+ }
+
+ private async Task GetToken(string subjectId, string appId, CancellationToken cancellationToken)
+ {
+ // TODO: Get actual token for subject and app
+ var accessTokenCacheKey = GetCacheKey(subjectId, appId, "access_token");
+ var accessToken = await _tokenCache.GetStringAsync(accessTokenCacheKey, cancellationToken);
+
+ if (accessToken is not null)
+ return accessToken;
+
+ const string newToken = "totallyARealToken"; // Replace with actual token retrieval logic
+ await _tokenCache.SetStringAsync(
+ accessTokenCacheKey,
+ newToken,
+ new DistributedCacheEntryOptions
+ {
+ AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(1),
+ },
+ cancellationToken
+ );
+ accessToken = newToken;
+
+ return accessToken;
+ }
+
+ private string GetCacheKey(string subjectId, string appId, string tokenType)
+ {
+ return $"{subjectId}:{appId}:{tokenType}";
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs
index e0c8dbf..ac40b6d 100644
--- a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs
+++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs
@@ -10,12 +10,10 @@ namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
public class AuthController : ControllerBase
{
private readonly IPalantirClient _client;
- private readonly IPalantirAuthClient _authClient;
- public AuthController(IPalantirClient client, IPalantirAuthClient authClient)
+ public AuthController(IPalantirClient client)
{
_client = client;
- _authClient = authClient;
}
[HttpGet("[action]")]
@@ -27,7 +25,7 @@ public class AuthController : ControllerBase
[HttpPost("[action]")]
public async Task Login([FromBody] LoginCommand loginCommand)
{
- var validationResult = await _client.Auth.ValidateCredentials(new ValidateCredentialsCommand { Password = loginCommand.Password, UsernameOrEmail = loginCommand.UsernameOrEmail });
+ var validationResult = await _client.Internal.Auth.ValidateCredentials(new ValidateCredentialsCommand { Password = loginCommand.Password, UsernameOrEmail = loginCommand.UsernameOrEmail });
return validationResult.Match(valid =>
{
diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Program.cs b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Program.cs
index 25f01b4..d7da371 100644
--- a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Program.cs
+++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Program.cs
@@ -1,8 +1,23 @@
+using Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
using Suspectus.Gandalf.Palantir.Client.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddPalantirClient();
+
+builder.Services.AddHttpClient("test")
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
+ {
+ ServerCertificateCustomValidationCallback =
+ HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ });
+
+builder.Services.AddStackExchangeRedisCache(opt =>
+{
+ var connectionString = builder.Configuration.GetConnectionString("Redis");
+ opt.Configuration = connectionString;
+});
+
builder.Services.AddControllers();
builder.Services.AddOpenApi();
diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Suspectus.Gandalf.Bridgekeeper.Api.csproj b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Suspectus.Gandalf.Bridgekeeper.Api.csproj
index cff35d2..78fe9a0 100644
--- a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Suspectus.Gandalf.Bridgekeeper.Api.csproj
+++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Suspectus.Gandalf.Bridgekeeper.Api.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/appsettings.Development.json b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/appsettings.Development.json
index 0c208ae..852c1b1 100644
--- a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/appsettings.Development.json
+++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/appsettings.Development.json
@@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "ConnectionStrings": {
+ "Redis": "localhost:6379"
}
}
diff --git a/src/dotnet/Suspectus.Gandalf.Core.Abstractions/DTOs/AppInfo.cs b/src/dotnet/Suspectus.Gandalf.Core.Abstractions/DTOs/AppInfo.cs
new file mode 100644
index 0000000..e0195c6
--- /dev/null
+++ b/src/dotnet/Suspectus.Gandalf.Core.Abstractions/DTOs/AppInfo.cs
@@ -0,0 +1,9 @@
+namespace Suspectus.Gandalf.Core.Abstractions.DTOs;
+
+public class AppInfo
+{
+ public required string Id { get; set; }
+ public required bool IsMaster { get; set; }
+ public required string Name { get; set; }
+ public required string BaseAddress { get; set; }
+}
\ No newline at end of file
diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AppController.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AppController.cs
new file mode 100644
index 0000000..385a7a8
--- /dev/null
+++ b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AppController.cs
@@ -0,0 +1,56 @@
+using System.ComponentModel.DataAnnotations;
+using HashidsNet;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Suspectus.Gandalf.Core.Abstractions.DTOs;
+using Suspectus.Gandalf.Palantir.Data.Database;
+
+namespace Suspectus.Gandalf.Palantir.Api.Controllers;
+
+[ApiController]
+[Route("api/internal/[controller]")]
+public class AppController : ControllerBase
+{
+ private readonly ApplicationContext _context;
+ private readonly IHashids _hashids;
+
+ public AppController(ApplicationContext context, IHashids hashids)
+ {
+ _context = context;
+ _hashids = hashids;
+ }
+
+ [HttpGet("info/{appId}")]
+ [AllowAnonymous]
+ public async Task GetInfo([FromRoute][Required] string appId, CancellationToken cancellationToken)
+ {
+ var decodedId = _hashids.DecodeSingleLong(appId);
+ var appInfo = await _context.Apps
+ .Where(x => x.Id == decodedId)
+ .Select(x => new AppInfo
+ {
+ Id = appId,
+ IsMaster = x.Tenant!.IsMaster,
+ Name = x.Name,
+ BaseAddress = x.Tenant!.IsMaster ? "http://localhost:5035/" : "null"
+ })
+ .SingleOrDefaultAsync(cancellationToken: cancellationToken);
+ return Ok(appInfo);
+ }
+
+ [HttpGet("info")]
+ [AllowAnonymous]
+ public async Task GetInfos(CancellationToken cancellationToken)
+ {
+ var appInfos = await _context.Apps.Select(x => new AppInfo
+ {
+ Id = _hashids.EncodeLong(x.Id!.Value),
+ IsMaster = x.Tenant!.IsMaster,
+ Name = x.Name,
+ BaseAddress = x.Tenant!.IsMaster ? "http://localhost:5035/" : "null"
+ }).ToListAsync(cancellationToken: cancellationToken);
+
+ return Ok(appInfos);
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AuthController.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AuthController.cs
index b946def..d4beec0 100644
--- a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AuthController.cs
+++ b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/AuthController.cs
@@ -6,7 +6,7 @@ using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
namespace Suspectus.Gandalf.Palantir.Api.Controllers;
[ApiController]
-[Route("api/[controller]")]
+[Route("api/internal/[controller]")]
public class AuthController(IMediator mediator) : ControllerBase
{
[HttpPost("[action]")]
diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/TenantController.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/TenantController.cs
new file mode 100644
index 0000000..fa3cbfb
--- /dev/null
+++ b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Controllers/TenantController.cs
@@ -0,0 +1,31 @@
+using System.ComponentModel.DataAnnotations;
+using HashidsNet;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Suspectus.Gandalf.Core.Abstractions.DTOs;
+using Suspectus.Gandalf.Palantir.Data.Database;
+
+namespace Suspectus.Gandalf.Palantir.Api.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+[Authorize]
+public class TenantController : ControllerBase
+{
+ private readonly ApplicationContext _context;
+ private readonly IHashids _hashids;
+
+ public TenantController(ApplicationContext context, IHashids hashids)
+ {
+ _context = context;
+ _hashids = hashids;
+ }
+
+ [HttpGet]
+ public async Task Get(CancellationToken cancellationToken)
+ {
+ var tenantEntities = await _context.Tenants.ToListAsync(cancellationToken: cancellationToken);
+ return Ok(tenantEntities);
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs
index 4d477e0..8b32f4d 100644
--- a/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs
+++ b/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs
@@ -6,15 +6,21 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPalantirClient(this IServiceCollection services)
{
- services.AddHttpClient(opt =>
- {
- opt.BaseAddress = new Uri("https://localhost:7269/api/auth/");
- })
- .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
- {
- ServerCertificateCustomValidationCallback =
- HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
- });
+ services.AddHttpClient(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/auth/"); })
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
+ {
+ ServerCertificateCustomValidationCallback =
+ HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ });
+
+ services.AddHttpClient(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/app/"); })
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
+ {
+ ServerCertificateCustomValidationCallback =
+ HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ });
+
+ services.AddScoped();
services.AddScoped();
return services;
}
diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs
index 6bf8f07..6e16e67 100644
--- a/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs
+++ b/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs
@@ -1,5 +1,6 @@
using System.Net.Http.Json;
using LanguageExt.Common;
+using Suspectus.Gandalf.Core.Abstractions.DTOs;
using Suspectus.Gandalf.Core.Abstractions.Extensions;
using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
@@ -7,29 +8,58 @@ namespace Suspectus.Gandalf.Palantir.Client;
public interface IPalantirClient
{
- IPalantirAuthClient Auth { get; init; }
+ IPalantirInternalClient Internal { get; init; }
}
-public interface IPalantirAuthClient
+public interface IPalantirInternalClient
+{
+ IPalantirInternalAuthClient Auth { get; init; }
+ IPalantirInternalAppClient App { get; init; }
+}
+
+public class PalantirInternalClient : IPalantirInternalClient
+{
+ public PalantirInternalClient(
+ IPalantirInternalAuthClient auth,
+ IPalantirInternalAppClient app
+ )
+ {
+ Auth = auth;
+ App = app;
+ }
+
+ public IPalantirInternalAuthClient Auth { get; init; }
+ public IPalantirInternalAppClient App { get; init; }
+}
+
+
+public interface IPalantirInternalAuthClient
{
public Task> ValidateCredentials(ValidateCredentialsCommand validateCredentialsCommand);
}
-public class PalantirClient : IPalantirClient
+public interface IPalantirInternalAppClient
{
- public PalantirClient(IPalantirAuthClient authClient)
- {
- Auth = authClient;
- }
-
- public IPalantirAuthClient Auth { get; init; }
+ public Task> GetInfo(string appId);
}
-public class PalantirAuthClient : IPalantirAuthClient
+public class PalantirClient : IPalantirClient
+{
+ public PalantirClient(
+ IPalantirInternalClient internalClient
+ )
+ {
+ Internal = internalClient;
+ }
+
+ public IPalantirInternalClient Internal { get; init; }
+}
+
+public class PalantirInternalAuthClient : IPalantirInternalAuthClient
{
private readonly HttpClient _http;
- public PalantirAuthClient(HttpClient http)
+ public PalantirInternalAuthClient(HttpClient http)
{
_http = http;
}
@@ -39,12 +69,12 @@ public class PalantirAuthClient : IPalantirAuthClient
try
{
var response = await _http.PostAsJsonAsync("validate-credentials", validateCredentialsCommand);
-
+
if (!response.IsSuccessStatusCode)
{
return $"status: {response.StatusCode}".AsErrorResult();
}
-
+
var result = await response.Content.ReadFromJsonAsync();
return result;
}
@@ -53,4 +83,40 @@ public class PalantirAuthClient : IPalantirAuthClient
return $"status: {e.StatusCode} message: {e.Message}".AsErrorResult();
}
}
+}
+
+public class PalantirInternalAppClient : IPalantirInternalAppClient
+{
+ private readonly HttpClient _http;
+
+ public PalantirInternalAppClient(HttpClient http)
+ {
+ _http = http;
+ }
+
+ public async Task> GetInfo(string appId)
+ {
+ try
+ {
+ var response = await _http.GetAsync($"info/{appId}");
+
+ if (!response.IsSuccessStatusCode)
+ {
+ return $"status: {response.StatusCode}".AsErrorResult();
+ }
+
+ var result = await response.Content.ReadFromJsonAsync();
+
+ if (result is null)
+ {
+ return "InternalApp not found.".AsErrorResult();
+ }
+
+ return result;
+ }
+ catch (HttpRequestException e)
+ {
+ return $"status: {e.StatusCode} message: {e.Message}".AsErrorResult();
+ }
+ }
}
\ No newline at end of file