Exemple #1
0
        /// <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);
        }
Exemple #2
0
        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(),
            });
        }
Exemple #3
0
            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;
                }
            }
Exemple #4
0
            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();
            }