using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.Session; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using zero.Core.Database; using zero.Core.Entities; using zero.Core.Extensions; using zero.Core.Handlers; using zero.Core.Identity; using zero.Core.Options; namespace zero.Core.Api { public class ApplicationContext : IApplicationContext { /// public IApplication App { get; protected set; } /// public string AppId { get; protected set; } protected IZeroStore Store { get; private set; } protected IZeroOptions Options { get; private set; } protected ILogger Logger { get; private set; } protected IHandlerHolder Handler { get; private set; } static IList Apps { get; set; } public ApplicationContext(IZeroStore store, IZeroOptions options, ILogger logger, IHandlerHolder handler = null) { Store = store; Options = options; Logger = logger; Handler = handler; } /// public async Task Resolve(HttpContext context, ClaimsPrincipal user) { if (context?.Request == null) { return null; } IApplication app; if (IsBackofficeRequest(context)) { app = await ResolveFromUser(user); } else { app = await ResolveFromRequest(context); } if (app == null) { //Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host); IList apps = await GetApplications(); app = apps.FirstOrDefault(); } App = app; AppId = app?.Id; return app; } /// public async Task TrySwitchForUser(IBackofficeUser user, string appId) { if (user == null || appId.IsNullOrEmpty()) { return false; } string[] allowedAppIds = GetAllowedAppIdsForUser(user); bool isMainId = false; // appId.Equals(user.AppId, StringComparison.InvariantCultureIgnoreCase); // TODO appx fix bool isAllowedId = allowedAppIds.Contains(appId, StringComparer.InvariantCultureIgnoreCase); if (user.IsSuper || isMainId || isAllowedId) { user.CurrentAppId = appId; //byte[] bytes = new byte[20]; //RandomNumberGenerator.Fill(bytes); //user.SecurityStamp = Base32.ToBase32(bytes); // TODO update security stamp but Base32 is .net core internal using IAsyncDocumentSession session = Store.OpenAsyncSession(); await session.StoreAsync(user); await session.SaveChangesAsync(); return true; } return false; } /// public async Task ResolveFromUser(ClaimsPrincipal user) { IBackofficeUser userEntity = await GetBackofficeUser(user); return await ResolveFromUser(userEntity); } /// public async Task ResolveFromUser(IBackofficeUser user) { if (user == null) { return null; } string appId = null; string[] allowedAppIds = GetAllowedAppIdsForUser(user); if (!user.CurrentAppId.IsNullOrEmpty()) { if (user.IsSuper || allowedAppIds.Contains(user.CurrentAppId, StringComparer.InvariantCultureIgnoreCase)) { appId = user.CurrentAppId; } else { //appId = user.AppId; // TODO appx fix } } else { //appId = user.AppId; // TODO appx fix } if (appId.IsNullOrEmpty()) { throw new Exception($"User entity ${user.Id} needs a valid AppId"); } using IAsyncDocumentSession session = Store.OpenAsyncSession(); return await session.LoadAsync(appId); } /// public async Task ResolveFromRequest(HttpContext context) { return Handler.Get()?.Resolve(context.Request, await GetApplications()) ?? await ResolveFromUri(context.Request.GetEncodedUrl()); } /// public async Task ResolveFromUri(string uriString) { return ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications()); } /// public async Task ResolveFromUri(Uri uri) { return ResolveFromUriInternal(uri, await GetApplications()); } /// public async Task ForId(string appId) { using IAsyncDocumentSession session = Store.OpenAsyncSession(); IApplication app = await session.LoadAsync(appId); return new ApplicationContext(Store, Options, Logger, Handler) { App = app, AppId = appId }; } /// public bool IsBackofficeRequest(HttpContext context) { string path = Options.BackofficePath.EnsureStartsWith('/').TrimEnd('/'); return context.Request.Path.ToString().StartsWith(path); } /// /// Get matching application from an URI /// IApplication ResolveFromUriInternal(Uri uri, IList apps) { string[] protocols = new string[3] { "https://", "http://", "//" }; IApplication currentApp = null; foreach (IApplication app in apps) { if (app.Domains?.Length < 1) { continue; } foreach (Uri domain in app.Domains) { //string normalizedDomain = domain; //if (!protocols.Any(protocol => domain.StartsWith(protocol, StringComparison.OrdinalIgnoreCase))) //{ // normalizedDomain = "http://" + domain; //} //UriBuilder uriBuilder = new UriBuilder(normalizedDomain); //if (!uriBuilder.Uri.IsAbsoluteUri) //{ // continue; //} int compareResult = Uri.Compare(uri, domain, UriComponents.HostAndPort, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase); if (compareResult == 0) { currentApp = app; break; } } } return currentApp; } /// /// Get all applications to choose from /// async Task> GetApplications() { if (Apps != null) { return Apps; } using (IAsyncDocumentSession session = Store.OpenAsyncSession()) { Apps = await session.Query().ToListAsync(); return Apps; } } /// /// Get backoffice user from claims principal /// async Task GetBackofficeUser(ClaimsPrincipal user) { string userId = user.FindFirstValue(Constants.Auth.Claims.UserId); using IAsyncDocumentSession session = Store.OpenAsyncSession(); return await session.LoadAsync(userId); } /// /// Get applications the user has access to /// string[] GetAllowedAppIdsForUser(IBackofficeUser user) { IEnumerable permissions = user.Claims .Where(claim => claim.Type == Constants.Auth.Claims.Permission && claim.Value.StartsWith(Permissions.Applications)) .Select(x => Permission.FromClaim(x.ToClaim(), Permissions.Applications)); string[] appIds = permissions.Where(x => x.IsTrue).Select(x => x.NormalizedKey).ToArray(); return appIds; } } public interface IApplicationContext { /// /// Currently loaded application /// IApplication App { get; } /// /// Current loaded application Id /// string AppId { get; } /// /// Resolves the current application from either the backoffice user (in case it is backoffice request) /// or the domain (in case it is frontend request). /// The resolved data is stored in the App + AppId properties. /// Task Resolve(HttpContext context, ClaimsPrincipal user); /// /// Try to switch the current application for a user /// Task TrySwitchForUser(IBackofficeUser user, string appId); /// /// Resolves the current application from the request path /// Task ResolveFromRequest(HttpContext context); /// /// Get matching application from an URI string /// Task ResolveFromUri(string uriString); /// /// Get matching application from an URI /// Task ResolveFromUri(Uri uri); /// /// Resolves the current application from the logged-in backoffice user. /// This method won't return apps the user has no access to. /// Task ResolveFromUser(ClaimsPrincipal user); /// /// Resolves the current application from a user. /// This method won't return apps the user has no access to. /// Task ResolveFromUser(IBackofficeUser user); /// /// Creates a new application context for the specified application. /// Task ForId(string appId); /// /// Whether the current request is a backoffice request /// bool IsBackofficeRequest(HttpContext context); } }