/// <summary> /// Initializes the connection if it is not already initialized /// </summary> /// <returns>The connection string that was initialized</returns> private async Task <SqlConnection> GetConnectionAsync(CancellationToken cancellation = default) { using var _ = _instrumentation.Block("AdminRepo." + nameof(GetConnectionAsync)); if (_conn == null) { _conn = new SqlConnection(_connectionString); await _conn.OpenAsync(cancellation); } if (_userInfo == null) { // Always call OnConnect SP as soon as you create the connection var externalUserId = _externalUserAccessor.GetUserId(); var externalEmail = _externalUserAccessor.GetUserEmail(); var culture = CultureInfo.CurrentUICulture.Name; var neutralCulture = CultureInfo.CurrentUICulture.IsNeutralCulture ? CultureInfo.CurrentUICulture.Name : CultureInfo.CurrentUICulture.Parent.Name; _userInfo = await OnConnect(externalUserId, externalEmail, culture, neutralCulture, cancellation); } // Since we opened the connection once, we need to explicitly enlist it in any ambient transaction // every time it is requested, otherwise commands will be executed outside the boundaries of the transaction _conn.EnlistInTransaction(transactionOverride: _transactionOverride); return(_conn); }
public async Task <CompaniesForClient> GetForClient(CancellationToken cancellation) { var companies = new ConcurrentBag <UserCompany>(); var externalId = _externalUserAccessor.GetUserId(); var externalEmail = _externalUserAccessor.GetUserEmail(); var(databaseIds, isAdmin) = await _repo.GetAccessibleDatabaseIds(externalId, externalEmail, cancellation); // Connect all the databases in parallel, ensure the user cann access them all var tasks = databaseIds.Select(databaseId => GetCompanyInfoAsync(databaseId, companies, cancellation)); await Task.WhenAll(tasks); // Confirm isAdmin by checking with the admin DB if (isAdmin) { var adminUserInfo = await _repo.GetAdminUserInfoAsync(cancellation); isAdmin = adminUserInfo?.UserId != null; } return(new CompaniesForClient { IsAdmin = isAdmin, Companies = companies.OrderBy(e => e.Id).ToList(), }); }
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { // (1) Make sure the API caller have provided a tenantId, and extract it try { var cancellation = context.HttpContext.RequestAborted; int tenantId = _tenantIdAccessor.GetTenantId(); // Init the database connection... // The client sometimes makes ambient API calls, not in response to user interaction // Such calls should not update LastAccess of that user bool silent = context.HttpContext.Request.Query["silent"].FirstOrDefault()?.ToString()?.ToLower() == "true"; await _appRepo.InitConnectionAsync(tenantId, setLastActive : !silent, cancellation); // (2) Make sure the user is a member of this tenant UserInfo userInfo = await _appRepo.GetUserInfoAsync(cancellation); if (userInfo.UserId == null) { // If there is no user cut the pipeline short and return a Forbidden 403 context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); // This indicates to the client to discard all cached information about this // company since the user is no longer a member of it context.HttpContext.Response.Headers.Add("x-settings-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-definitions-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-permissions-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-user-settings-version", Constants.Unauthorized); return; } var userId = userInfo.UserId.Value; var externalId = _externalUserAccessor.GetUserId(); var externalEmail = _externalUserAccessor.GetUserEmail(); // (3) If the user exists but new, set the External Id if (userInfo.ExternalId == null) { // Update external Id in this tenant database await _appRepo.Users__SetExternalIdByUserId(userId, externalId); // Update external Id in the central Admin database too (To avoid an awkward situation // where a user exists on the tenant but not on the Admin db, if they change their email in between) var adminRepo = _serviceProvider.GetRequiredService <AdminRepository>(); await adminRepo.DirectoryUsers__SetExternalIdByEmail(externalEmail, externalId); } else if (userInfo.ExternalId != externalId) { // Note: there is the edge case of identity providers who allow email recycling. I.e. we can get the same email twice with // two different external Ids. This issue is so unlikely to naturally occur and cause problems here that we are not going // to handle it for now. It can however happen artificually if the application is re-configured to a new identity provider, // or if someone messed with the identity database directly, but again out of scope for now. context.Result = new BadRequestObjectResult("The sign-in email already exists but with a different external Id"); return; } // (4) If the user's email address has changed at the identity server, update it locally else if (userInfo.Email != externalEmail) { await _appRepo.Users__SetEmailByUserId(userId, externalEmail); } // (5) Set the tenant info in the context, to make it accessible for model metadata providers var tenantInfo = await _appRepo.GetTenantInfoAsync(cancellation); _tenantInfoAccessor.SetInfo(tenantId, tenantInfo); // (6) Ensure the freshness of the definitions and settings caches { var databaseVersion = tenantInfo.DefinitionsVersion; var serverVersion = _definitionsCache.GetDefinitionsIfCached(tenantId)?.Version; if (serverVersion == null || serverVersion != databaseVersion) { // Update the cache var definitions = await DefinitionsService.LoadDefinitionsForClient(_appRepo, cancellation); if (!cancellation.IsCancellationRequested) { _definitionsCache.SetDefinitions(tenantId, definitions); } } } { var databaseVersion = tenantInfo.SettingsVersion; var serverVersion = _settingsCache.GetSettingsIfCached(tenantId)?.Version; if (serverVersion == null || serverVersion != databaseVersion) { // Update the cache var settings = await GeneralSettingsService.LoadSettingsForClient(_appRepo, cancellation); if (!cancellation.IsCancellationRequested) { _settingsCache.SetSettings(tenantId, settings); } } } // (7) If any version headers are supplied: examine their freshness { // Permissions var clientVersion = context.HttpContext.Request.Headers["X-Permissions-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.PermissionsVersion; context.HttpContext.Response.Headers.Add("x-permissions-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // User Settings var clientVersion = context.HttpContext.Request.Headers["X-User-Settings-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.UserSettingsVersion; context.HttpContext.Response.Headers.Add("x-user-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // Definitions var clientVersion = context.HttpContext.Request.Headers["X-Definitions-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = tenantInfo.DefinitionsVersion; context.HttpContext.Response.Headers.Add("x-definitions-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // Settings var clientVersion = context.HttpContext.Request.Headers["X-Settings-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = tenantInfo.SettingsVersion; context.HttpContext.Response.Headers.Add("x-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } // Call the Action itself await next(); } catch (TaskCanceledException) { context.Result = new OkResult(); return; } catch (MultitenancyException ex) { // If the tenant Id is not provided cut the pipeline short and return a Bad Request 400 context.Result = new BadRequestObjectResult(ex.Message); return; } catch (BadRequestException ex) { // If the tenant Id is not provided cut the pipeline short and return a Bad Request 400 context.Result = new BadRequestObjectResult(ex.Message); return; } catch (Exception ex) { // TODO: Return to logging and 500 status code context.Result = new BadRequestObjectResult(ex.GetType().Name + ": " + ex.Message); //_logger.LogError(ex.Message); //context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError); return; } }
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { var cancellation = context.HttpContext.RequestAborted; // (1) Make sure the requester has an active user AdminUserInfo userInfo = await _adminRepo.GetAdminUserInfoAsync(cancellation); if (userInfo.UserId == null) { // If there is no user cut the pipeline short and return a Forbidden 403 context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); // This indicates to the client to discard all cached information about this // company since the user is no longer a member of it context.HttpContext.Response.Headers.Add("x-admin-settings-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-admin-permissions-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-admin-user-settings-version", Constants.Unauthorized); return; } var userId = userInfo.UserId.Value; var externalId = _externalUserAccessor.GetUserId(); var externalEmail = _externalUserAccessor.GetUserEmail(); // (3) If the user exists but new, set the External Id if (userInfo.ExternalId == null) { using var trx = ControllerUtilities.CreateTransaction(); await _adminRepo.AdminUsers__SetExternalIdByUserId(userId, externalId); await _adminRepo.DirectoryUsers__SetExternalIdByEmail(externalEmail, externalId); trx.Complete(); } else if (userInfo.ExternalId != externalId) { // Note: we will assume that no identity provider can provider the same email twice with // two different external Ids, i.e. that no provider allows email recycling, so we won't handle this case now // This can only happen if the application is re-configured to a new identity provider, or if someone messed with // the database directly context.Result = new BadRequestObjectResult("The sign-in email already exists but with a different external Id"); return; } // (4) If the user's email address has changed at the identity server, update it locally else if (userInfo.Email != externalEmail) { using var trx = ControllerUtilities.CreateTransaction(); await _adminRepo.AdminUsers__SetEmailByUserId(userId, externalEmail); await _adminRepo.DirectoryUsers__SetEmailByExternalId(externalId, externalEmail); trx.Complete(); } // (5) If any version headers are supplied: examine their freshness { // Permissions var clientVersion = context.HttpContext.Request.Headers["X-Admin-Permissions-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.PermissionsVersion; context.HttpContext.Response.Headers.Add("x-admin-permissions-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // User Settings var clientVersion = context.HttpContext.Request.Headers["X-Admin-User-Settings-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.UserSettingsVersion; context.HttpContext.Response.Headers.Add("x-admin-user-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // Settings var clientVersion = context.HttpContext.Request.Headers["X-Admin-Settings-Version"].FirstOrDefault(); var adminInfo = new { SettingsVersion = clientVersion }; // await _adminRepo.GetAdminInfoAsync(); // TODO if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = adminInfo.SettingsVersion; context.HttpContext.Response.Headers.Add("x-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } // Finally call the Action itself await next(); }