abpframework / abp

Open Source Web Application Framework for ASP.NET Core. Offers an opinionated architecture to build enterprise software solutions with best practices on top of the .NET and the ASP.NET Core platforms. Provides the fundamental infrastructure, production-ready startup templates, application modules, UI themes, tooling, guides and documentation.
https://abp.io
GNU Lesser General Public License v3.0
12.31k stars 3.32k forks source link

Impersonating users #1082

Closed Trojaner closed 2 years ago

Trojaner commented 5 years ago

Whats the current recommended way of impersonating users?

hikalkan commented 5 years ago

Haven't thought about this.

softwareguy74 commented 4 years ago

This is definitely a must in any real production environment. There issues that creep up in production that the current user might see, that the administrator may not. Being able to impersonate a user almost always results in being able to "see" the problem. I say this with experience in a large corporate environment.

"Impersonate User" should mean I can select from a list of Tenant/User and then from the system perspective, I AM that user. Of course, this means not needing a password, just the fact that the system now thinks that I am that particular user.

hikalkan commented 4 years ago

I agree that is is an important feature (and we have implemented it for aspnet zero before). However, we currently have more prioritized items to work on. We will think on that later.

olicooper commented 4 years ago

Now that v1.0 has been released, I wondered if this might be reconsidered?

I've seen similar issues raised before:

hikalkan commented 4 years ago

Labeled as a high priority item in the backlog. If that's urgent for you, I suggest you to implement it yourself.

olicooper commented 4 years ago

For anyone who is interested... My current working implementation can transparently impersonate a Tenant if you are logged in as a HOST user. Because we have access to ICurrentUser.TenantId but also ICurrentTenant we can exploit this to impersonate other tenants...

First we create a request middleware to configure the 'ICurrentTenant' if we find a custom __tenant header or query parameter:

class CustomCurrentUserTenantResolveContributor : CurrentUserTenantResolveContributor
{
    public override void Resolve(ITenantResolveContext context)
    {
        var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
        if (currentUser.IsAuthenticated != true)
        {
            return;
        }

        context.Handled = true;
        context.TenantIdOrName = currentUser.TenantId?.ToString();

        // If is host, then check from custom tenantId in request header and query...
        // See "HeaderTenantResolveContributor" and "QueryStringTenantResolveContributor"
        var httpContext = Volo.Abp.ServiceProviderAccessorExtensions.GetHttpContext(context);
        if (currentUser.TenantId == null)
        {
            if (httpContext.Request != null)
            {
                var tenantIdKey = context.GetAbpAspNetCoreMultiTenancyOptions().TenantKey;
                Guid tenantId = default;

                // Look in header
                if (httpContext.Request.Headers[tenantIdKey].Count >= 1)
                {
                    if (!Guid.TryParse(httpContext.Request.Headers[tenantIdKey].Last(), out tenantId)) return;
                }
                // Look in query string
                else if (httpContext.Request.QueryString.HasValue)
                {
                    if (!Guid.TryParse(httpContext.Request.Query[tenantIdKey], out tenantId)) return;
                }

                if (tenantId == default) return;

                var _settingManager = context.ServiceProvider.GetRequiredService<ISettingManager>();
                // todo: should we be getting the current tenant inside the tenant resolver? Will this break things?
                var _currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
                using (_currentTenant.Change(tenantId))
                {
                    if (Convert.ToBoolean(
                        // todo: will this cause deadlocks or reduce available threads? We probably need to find an alternative!
                        AsyncHelper.RunSync(() => _settingManager.GetOrNullForCurrentTenantAsync("App.AllowSupportAccess"))
                    ))
                    {
                        context.TenantIdOrName = tenantId.ToString();
                    }
                }
            }
        }
    }
}

Within the ConfigureServices method of the HttpApiHostModule:

// Allows hosts to impersonate tenants
Configure<AbpTenantResolveOptions>(options =>
{
    options.TenantResolvers.ReplaceOne(
        x => x.GetType() == typeof(CurrentUserTenantResolveContributor), 
        new CustomCurrentUserTenantResolveContributor());
});

Then simply add the __tenant header with a valid TenantId to each request. The negative of this approach is that the audit logs aren't correct as the ImpersonatorUserId and ImpersonatorTenantId aren't populated.

Also @hikalkan can I get your opinion on this approach? Any improvements or recommendations?

bhyatz commented 4 years ago

@hikalkan. What is the timeline for this feature to be added?

LuisPignataro commented 3 years ago

Hi @hikalkan , could you give me some information about what is the correct way to set the CurrentUser in a process that is executed as a task? For example, when running an Azure backgroundJob or WebJob that calls an AppService, it needs to be done with credentials. I can't find the correct way to do it, I have searched the code, tests and documentation without success. From already thank you very much.

maliming commented 3 years ago

hi @LuisPignataro

Changing the Current Principle https://docs.abp.io/en/abp/latest/CurrentUser#changing-the-current-principle

jogoertzen-stantec commented 3 years ago

Is this still on track for a 4.1-preview release? We are currently evaluating ABP Framework and time is limited. :)

maliming commented 3 years ago

hi @jogoertzen-stantec

Because there may be some limitations in tiered projects. I can't guarantee that it will work in 4.1, but I will try my best.

ghost commented 3 years ago

We are waiting this feature also

DmezaSpeed commented 3 years ago

We are new but waiting to for this feature, we need a dead line!

maliming commented 3 years ago

@DmezaSpeed https://github.com/abpframework/abp/issues/1082#issuecomment-731579375

dicksonkimeu commented 3 years ago

hi @LuisPignataro

Changing the Current Principle https://docs.abp.io/en/abp/latest/CurrentUser#changing-the-current-principle

@maliming this sample code creates a new user. How about if the user already exists? How can one use it ?..

danielmeza commented 3 years ago

@maliming Hi! I see this has move throw 3 milestones now, witch is a real time line for this feature, we need to make a decision based on a dead line for this! please be honest and tanks for you effort.

maliming commented 3 years ago

For monolithic applications, this can be achieved, but we also have tiered applications, we have not been able to find a perfect solution.

danielmeza commented 3 years ago

Yo can share the state of investigation on this feature so we can all contribute with a solution?

maliming commented 3 years ago

If the application uses local authentication, such as cookies or self-signed JWT. We can easily switch the identity of the current user, refresh the cookies, and get the token again.

If the application uses remote authentication such as OpenID, switching the current identity is complicated.

danielmeza commented 3 years ago

Ok so the issue that should be resolved is to find out a secure and abstract way to impersonate the user on an external Identity provider, where reside the difficult on the remote authentication way? On make the authentication process against the server or on impersonate te user internally after the authentication process has been complete? With identity server we can have back channels in order to make the authentication easy and secure, or we can expose a custom endpoint for that using a middleware and including a well explained documentation on how to implement it so other Identity providers can easily add it.

cbogner85 commented 3 years ago

I agree that this is a very important feature for every professional application.

Often a customer tells you there is something wrong in his account and without impersonating it is hard to help. I really loved the feature in ASP.NET Zero.

You said that there are problems with tiered apps. Maybe you could implement it at least for monolith applications until you find a solution?

I thought about implementing it myself, however, I couldn't find a way to impersonate the user. Currently we can only change the current tenant. Setting the current principle by using his claims doesn't seem to work (it seems to create a new principle).

Thanks and keep up the great work :)

danielmeza commented 3 years ago

@maliming any update on this? Is still committed for the 4.3 release?

maliming commented 3 years ago

Still on the road map. If you are in a hurry, you can implement it yourself according to the actual situation. As I said above, different layered projects have different challenges.

danielmeza commented 3 years ago

@maliming then this should be put at the back log until we had a real stimate, instead of just dropped to the next release and create false expectations.

tvddev commented 3 years ago

Sorry didn't realise this was already an open issue when we created #8533.

I would like to add that we purchased ABP because it is a SaaS framework. Being able to impersonate a tenant user is such a basic function and is a really fundamental piece of functionality.

Having come from aspnetzero where support users use it every day, I don't understand how something so basic could be bumped for almost 2 years??

tvddev commented 3 years ago

@maliming - I did more reading on other items and merges, lots of people are trying to work around this limitation. Do you have a suggested workaround for a v4.3 based project?

maliming commented 3 years ago

hi @tvddev

We will do more work on 4.4 commercial. https://docs.abp.io/en/commercial/4.3/road-map

tvddev commented 3 years ago

@maliming thakyou for the update, will track progress in v4.4 preview.

Slightly off topic but I see you are also planning work around editions and payment subscriptions. Can you point me to the issue # or explain what that SOW will include as we are doing some work with stripe too and it would be good to know how far we should go pending v4.4 preview.

dicksonkimeu commented 3 years ago

@maliming am also interested know the subscriptions and payments feature. Does it address the normal SaaS subscriptions with licensed users count control. Just like any other SaaS app ?

This would be great since any SaaS app needs this.

maliming commented 3 years ago

v4.4 Planned release date: End of Quarter 2, 2021.

@tvddev @dicksonkimeu We can discuss details on https://support.abp.io/

StevenOwenNell commented 2 years ago

Hi Guys, Are we still on track for the end of Quarter 2?

maliming commented 2 years ago

hi @StevenOwenNell

This feature will be available in the abp commercial.(v4.4)

lukebooroo commented 2 years ago

Hi,

I've just updated to 4.4 using the suite. I can't find how to use this? Thanks

maliming commented 2 years ago

hi @lukebooroo

We have implemented this feature and are solving the blazor problem, and it will be available soon.

Tenant impersonation: image image

User impersonation: image image image image image

tvddev commented 2 years ago

When will this be working in Blazor? Is there another github issue to fix for Blazor so we can track? we are trying to schedule resources to do the code upgrade and would prefer to only do it once from v4.3 to v4.4.

maliming commented 2 years ago

The main challenge is that Blazor does not yet support such features.

tvddev commented 2 years ago

@maliming ...Woah.. what do you mean? is this feature in v4.4 for Blazor?

maliming commented 2 years ago

Now it works for non-tiered MVC and Angular. Not works for tiered MVC and Blazor WASM that using OpenID Connect. For them, the login with tenant/user option is currently hidden.

For Blazor WASM, we can't customize its authentication components yet.

tvddev commented 2 years ago

Does it work for Blazor WASM without OpenID?

maliming commented 2 years ago

No, but it works for Blazor Server.

tvddev commented 2 years ago

Oh No!! we have been waiting for over a year for this feature, nobody said it would not be included for Blazor WASM! it is critical for a SaaS product.

What is the plan to deliver this for Blazor WASM?? @maliming @hikalkan

maliming commented 2 years ago

We also hope to implement it soon, but Blazor WASM doesn't support customize authentication components.

tvddev commented 2 years ago

But cant you create a customised authentication using Microsoft.AspNetCore.Components.Authorization ?

maliming commented 2 years ago

We are looking for all possible solutions.

MichelZ commented 2 years ago

So Tiered + Angular is also not available? If that's true, what's the plan there? Any ETA?

maliming commented 2 years ago

Not works for projects that use OpenID Connect for authentication. Blazor Wasm and https://docs.abp.io/en/abp/latest/Startup-Templates/Application#web-project-1

Angular will work.

tvddev commented 2 years ago

@maliming @hikalkan So what is the plan and ETA for Blazor WASM?

Are you reopening this gihub issue and adding to v4.4patch?

lukebooroo commented 2 years ago

For anyone who is desperate enough at this point, I've made this appallingly bad bodge solution. It's specifically to work if you're using the current user as the tenant resolver, but I'm sure it could be made to work another way. If anyone's got some input on how to make this better it would also be much appreciated.

I'm also working on some overrides so that the host can manage all users without switching tenant, since it seems quite laborious considering that on a support desk you've usually only got an email address to work with.

`
[ExposeServices(typeof(HttpContextCurrentPrincipalAccessor), typeof(ICurrentPrincipalAccessor))] [Dependency(ReplaceServices = true)] public class ImpersonationPrincipalAccessor : HttpContextCurrentPrincipalAccessor { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IIdentityUserRepository _identityUserRepository; private readonly IDataFilter _dataFilter; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IPermissionGrantRepository _permissionGrantRepository; private readonly IIdentityRoleRepository _identityRoleRepository;

    public ImpersonationPrincipalAccessor(
        IHttpContextAccessor httpContextAccessor, 
        IIdentityUserRepository identityUserRepository,
        IDataFilter dataFilter,
        IUnitOfWorkManager unitOfWorkManager,
        IPermissionGrantRepository permissionGrantRepository,
        IIdentityRoleRepository identityRoleRepository
        ) 
        : base(httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
        _identityUserRepository = identityUserRepository;
        _dataFilter = dataFilter;
        _unitOfWorkManager = unitOfWorkManager;
        _permissionGrantRepository = permissionGrantRepository;
        _identityRoleRepository = identityRoleRepository;
    }

    private Guid? GetImpersonatingId()
    {
        var header = _httpContextAccessor?.HttpContext?.Request.Headers["impersonate-user"];

        return !string.IsNullOrEmpty(header) ? Guid.Parse(header) : null;
    }

    protected override ClaimsPrincipal GetClaimsPrincipal()
    {
        var current = base.GetClaimsPrincipal();

        var impersonatingUser = GetImpersonatingId();

        if (impersonatingUser == null)
        {
            return current;
        }

        return ImpersonateUser(current, (Guid)impersonatingUser).WaitAndUnwrapException();
    }

    private async Task<ClaimsPrincipal> ImpersonateUser(ClaimsPrincipal principal, Guid impersonateUserId)
    {
        using (var uow = _unitOfWorkManager.Begin())
        using (_dataFilter.Disable<IMultiTenant>())
        {
            var actualUserId = principal.Claims.First(x => x.Type == AbpClaimTypes.UserId).Value;
            var actualUser = await _identityUserRepository.GetAsync(Guid.Parse(actualUserId));
            var impersonateUser = await _identityUserRepository.GetAsync(impersonateUserId);

            if (!await ImpersonationAllowed(actualUser, impersonateUser))
            {
                return principal;
            }

            var identity = new ClaimsIdentity(ConfirmUserModel.ConfirmUserScheme);
            identity.AddClaim(new Claim(AbpClaimTypes.UserId , impersonateUser.Id.ToString()));
            identity.AddClaim(new Claim(AbpClaimTypes.TenantId , impersonateUser.TenantId.ToString()));
            identity.AddClaim(new Claim(AbpClaimTypes.ImpersonatorUserId , actualUser.Id.ToString()));
            identity.AddClaim(new Claim(AbpClaimTypes.ImpersonatorTenantId , actualUser.TenantId.ToString()));

            principal = new ClaimsPrincipal(identity);

            await uow.CompleteAsync();

            return principal;
        }
    }

    private async Task<bool> ImpersonationAllowed(IdentityUser actualUser, IdentityUser impersonateUser)
    {
        var userRoles = actualUser.Roles.Select(x => x.RoleId);
        var userRoleNames = (await _identityRoleRepository.GetDbSetAsync())
            .Where(x => userRoles.Contains(x.Id)).Select(x => x.Name).ToList();

        var hasPermission = (await _permissionGrantRepository.GetDbSetAsync())
            .Where(x => 
                userRoleNames.Contains(x.ProviderKey) 
                && x.ProviderName == RolePermissionValueProvider.ProviderName
                && x.TenantId == actualUser.TenantId
            )
            .Any(x => x.Name == ShoutPermissions.Users.Impersonate);

        if (!hasPermission)
        {
            return false;
        }

        if (actualUser.TenantId == null)
        {
            // They're the host, they can impersonate whoever
            return true;
        }

        // Otherwise they should only be able to impersonate people in their own tenant.
        return actualUser.TenantId == impersonateUser.TenantId;
    }
}

`

cbogner85 commented 2 years ago

@maliming:

Just upgraded my project to 4.4.0 (stable) but can't find Tenant Impersonation (MVC, not tiered). Even tried to create a complete new project, but all the impersonation options from your screenshots are missing. What am I doing wrong?

Roles Tenant User

Cheers Claus

maliming commented 2 years ago

@cbogner85 Maybe 4.4.1 or 4.4.2. It will be available this month.