diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/CacheKey.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/CacheKey.cs index 6d63a7cb7..bb20f953f 100644 --- a/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/CacheKey.cs +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Infrastructure/Constants/CacheKey.cs @@ -32,6 +32,7 @@ public static class CacheKey const string EMAIL_UPDATE_SEND_PRE = "email_update_send:"; public const string STAFF_DEFAULT_PASSWORD = "staff_default_password"; const string MSG_VERIFIY_CODE_SEND_EXPIRED = "msg_verifiy_code_send_expired:"; + const string IMPERSONATION_USER= "impersonation_user:"; public static string AllPermissionKey() { @@ -162,4 +163,9 @@ public static string MsgVerifiyCodeSendExpired(string key) { return $"{MSG_VERIFIY_CODE_SEND_EXPIRED}{key}"; } + + public static string ImpersonationUserKey(string impersonationToken) + { + return $"{IMPERSONATION_USER}{impersonationToken}"; + } } diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Masa.Auth.Contracts.Admin.csproj b/src/Contracts/Masa.Auth.Contracts.Admin/Masa.Auth.Contracts.Admin.csproj index b7640bde4..700dad09c 100644 --- a/src/Contracts/Masa.Auth.Contracts.Admin/Masa.Auth.Contracts.Admin.csproj +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Masa.Auth.Contracts.Admin.csproj @@ -12,12 +12,12 @@ - + - + diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Subjects/ImpersonateInput.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Subjects/ImpersonateInput.cs new file mode 100644 index 000000000..b6946728b --- /dev/null +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Subjects/ImpersonateInput.cs @@ -0,0 +1,9 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Contracts.Admin.Subjects; + +public class ImpersonateInput +{ + public Guid UserId { get; set; } +} diff --git a/src/Contracts/Masa.Auth.Contracts.Admin/Subjects/ImpersonateOutput.cs b/src/Contracts/Masa.Auth.Contracts.Admin/Subjects/ImpersonateOutput.cs new file mode 100644 index 000000000..7e08fc418 --- /dev/null +++ b/src/Contracts/Masa.Auth.Contracts.Admin/Subjects/ImpersonateOutput.cs @@ -0,0 +1,9 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Contracts.Admin.Subjects; + +public class ImpersonateOutput +{ + public string ImpersonationToken { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/CommandHandler.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/CommandHandler.cs index 85bbb4dcd..52346b9de 100644 --- a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/CommandHandler.cs +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/CommandHandler.cs @@ -22,6 +22,7 @@ public class CommandHandler private readonly IUnitOfWork _unitOfWork; private readonly LdapDomainService _ldapDomainService; private readonly RoleDomainService _roleDomainService; + private readonly IUserContext _userContext; public CommandHandler( IUserRepository userRepository, @@ -40,7 +41,8 @@ public CommandHandler( IMasaConfiguration masaConfiguration, IUnitOfWork unitOfWork, LdapDomainService ldapDomainService, - RoleDomainService roleDomainService) + RoleDomainService roleDomainService, + IUserContext userContext) { _userRepository = userRepository; _autoCompleteClient = autoCompleteClient; @@ -59,6 +61,7 @@ public CommandHandler( _unitOfWork = unitOfWork; _ldapDomainService = ldapDomainService; _roleDomainService = roleDomainService; + _userContext = userContext; } #region User @@ -615,4 +618,31 @@ public async Task SaveUserClaimValuesAsync(SaveUserClaimValuesCommand saveUserCl await _userDomainService.UpdateAsync(user); } + + [EventHandler] + public async Task ImpersonateAsync(ImpersonateUserCommand command) + { + var userId = _userContext.GetUserId(); + if (userId == default) + { + throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.USER_NOT_EXIST); + } + var cacheItem = new ImpersonationCacheItem( + command.UserId, + command.IsBackToImpersonator + ); + + if (!command.IsBackToImpersonator) + { + cacheItem.ImpersonatorUserId = userId; + } + + var token = Guid.NewGuid().ToString(); + var key = CacheKey.ImpersonationUserKey(token); + await _distributedCacheClient.SetAsync(key, cacheItem, TimeSpan.FromMinutes(1)); + + command.Result = new ImpersonateOutput { + ImpersonationToken = token + }; + } } diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ImpersonateUserCommand.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ImpersonateUserCommand.cs new file mode 100644 index 000000000..e1965f186 --- /dev/null +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Commands/ImpersonateUserCommand.cs @@ -0,0 +1,9 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Service.Admin.Application.Subjects.Commands; + +public record ImpersonateUserCommand(Guid UserId, bool IsBackToImpersonator) : Command +{ + public ImpersonateOutput Result { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Queries/ImpersonatedUserQuery.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Queries/ImpersonatedUserQuery.cs new file mode 100644 index 000000000..f7cf3ebe6 --- /dev/null +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/Queries/ImpersonatedUserQuery.cs @@ -0,0 +1,9 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Service.Admin.Application.Subjects.Queries; + +public record ImpersonatedUserQuery(string ImpersonationToken) : Query +{ + public override ImpersonationCacheItem Result { get; set; } = new(); +} diff --git a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/QueryHandler.cs b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/QueryHandler.cs index 99ee76bf9..88e10fab0 100644 --- a/src/Services/Masa.Auth.Service.Admin/Application/Subjects/QueryHandler.cs +++ b/src/Services/Masa.Auth.Service.Admin/Application/Subjects/QueryHandler.cs @@ -775,4 +775,19 @@ public async Task UserClaimValuesQueryAsync(UserClaimValuesQuery userClaimValues userClaimValuesQuery.Result.TryAdd("userName", user.DisplayName); } } + + [EventHandler] + public async Task GetImpersonatedUserAsync(ImpersonatedUserQuery query) + { + var key = CacheKey.ImpersonationUserKey(query.ImpersonationToken); + var cacheItem = await _distributedCacheClient.GetAsync(key); + if (cacheItem == null) + { + throw new UserFriendlyException(errorCode: UserFriendlyExceptionCodes.IMPERSONATION_TOKEN_ERROR_MESSAGE); + } + + query.Result = cacheItem; + + await _distributedCacheClient.RemoveAsync(key); + } } diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/CacheModels/ImpersonationCacheItem.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/CacheModels/ImpersonationCacheItem.cs new file mode 100644 index 000000000..3d8cc693d --- /dev/null +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/CacheModels/ImpersonationCacheItem.cs @@ -0,0 +1,25 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Service.Admin.Infrastructure.CacheModels; + +[Serializable] +public class ImpersonationCacheItem +{ + public Guid ImpersonatorUserId { get; set; } + + public Guid TargetUserId { get; set; } + + public bool IsBackToImpersonator { get; set; } + + public ImpersonationCacheItem() + { + + } + + public ImpersonationCacheItem(Guid targetUserId, bool isBackToImpersonator) + { + TargetUserId = targetUserId; + IsBackToImpersonator = isBackToImpersonator; + } +} \ No newline at end of file diff --git a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs index a33174835..37126f041 100644 --- a/src/Services/Masa.Auth.Service.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs +++ b/src/Services/Masa.Auth.Service.Admin/Infrastructure/Constants/UserFriendlyExceptionCodes.cs @@ -91,4 +91,5 @@ public static class UserFriendlyExceptionCodes public const string IDENTITY_SOURCE_NAME_EXIST = "IdentitySourceNameExist"; public const string THIRDPARTYUSER_BIND_EXIST = "ThirdPartyUserBindExist"; public const string WEBHOOK_NOT_EXIST = "WebhookNotExist"; + public const string IMPERSONATION_TOKEN_ERROR_MESSAGE = "ImpersonationTokenErrorMessage"; } diff --git a/src/Services/Masa.Auth.Service.Admin/Services/UserService.cs b/src/Services/Masa.Auth.Service.Admin/Services/UserService.cs index 87b24cfd5..9fb2f4b8d 100644 --- a/src/Services/Masa.Auth.Service.Admin/Services/UserService.cs +++ b/src/Services/Masa.Auth.Service.Admin/Services/UserService.cs @@ -1,6 +1,8 @@ // Copyright (c) MASA Stack All rights reserved. // Licensed under the Apache License. See LICENSE.txt in the project root for license information. +using Nest; + namespace Masa.Auth.Service.Admin.Services; public class UserService : ServiceBase @@ -381,4 +383,21 @@ public async Task SaveClaimValuesAsync(IEventBus eventBus, UserClaimValuesDto us var command = new SaveUserClaimValuesCommand(userClaimValues.UserId, userClaimValues.ClaimValues); await eventBus.PublishAsync(command); } + + [RoutePattern("impersonate", StartWithBaseUri = true, HttpMethod = "Post")] + public async Task ImpersonateAsync(IEventBus eventBus, [FromBody] ImpersonateInput input) + { + var command = new ImpersonateUserCommand(input.UserId, false); + await eventBus.PublishAsync(command); + return command.Result; + } + + [AllowAnonymous] + [RoutePattern("impersonate", StartWithBaseUri = true, HttpMethod = "Get")] + public async Task GetImpersonatedUserAsync([FromServices] IEventBus eventBus, [FromQuery] string impersonationToken) + { + var query = new ImpersonatedUserQuery(impersonationToken); + await eventBus.PublishAsync(query); + return query.Result; + } } diff --git a/src/Web/Masa.Auth.Security.OAuth.Providers/Masa.Auth.Security.OAuth.Providers.csproj b/src/Web/Masa.Auth.Security.OAuth.Providers/Masa.Auth.Security.OAuth.Providers.csproj index d2509e9b8..c32f9f9b5 100644 --- a/src/Web/Masa.Auth.Security.OAuth.Providers/Masa.Auth.Security.OAuth.Providers.csproj +++ b/src/Web/Masa.Auth.Security.OAuth.Providers/Masa.Auth.Security.OAuth.Providers.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Web/Masa.Auth.Web.Admin.Rcl/Pages/Subjects/Users/User.razor b/src/Web/Masa.Auth.Web.Admin.Rcl/Pages/Subjects/Users/User.razor index 22c6753bf..168bcbf87 100644 --- a/src/Web/Masa.Auth.Web.Admin.Rcl/Pages/Subjects/Users/User.razor +++ b/src/Web/Masa.Auth.Web.Admin.Rcl/Pages/Subjects/Users/User.razor @@ -67,12 +67,15 @@ @IconConstants.Update - + @IconConstants.Authorize - + @IconConstants.Claim + + mdi-login-variant + break; default: diff --git a/src/Web/Masa.Auth.Web.Admin.Rcl/wwwroot/i18n/zh-CN.json b/src/Web/Masa.Auth.Web.Admin.Rcl/wwwroot/i18n/zh-CN.json index d5470acf2..f06d144cd 100644 --- a/src/Web/Masa.Auth.Web.Admin.Rcl/wwwroot/i18n/zh-CN.json +++ b/src/Web/Masa.Auth.Web.Admin.Rcl/wwwroot/i18n/zh-CN.json @@ -663,5 +663,6 @@ "Cancel Filter": "取消筛选", "RepeatAdd": "重复添加", "Add Claim": "添加声明", - "Claim": "声明" + "Claim": "声明", + "ImpersonationLogin": "模拟登录" } diff --git a/src/Web/Masa.Auth.Web.Admin.Server/Properties/launchSettings.json b/src/Web/Masa.Auth.Web.Admin.Server/Properties/launchSettings.json index 3a001d8cc..2c579ff28 100644 --- a/src/Web/Masa.Auth.Web.Admin.Server/Properties/launchSettings.json +++ b/src/Web/Masa.Auth.Web.Admin.Server/Properties/launchSettings.json @@ -22,8 +22,8 @@ "TLS_NAME": "", "MASA_CLUSTER": "Default", "OTLP_URL": "https://otel-collector.masastack:9013", - "REDIS": "{\"RedisHost\": \"10.130.0.235\", \"RedisPort\": 24615, \"RedisDb\": 0,\"RedisPassword\": \"Hzss@123\"}", - "CONNECTIONSTRING": "{\"Server\": \"10.130.0.235\", \"Port\": 24878,\"Database\":\"pm-dev\",\"UserId\": \"ss\",\"Password\":\"Hzss@123\"}", + "REDIS": "{\"RedisHost\": \"10.130.0.19\", \"RedisPort\": 2135, \"RedisDb\": 3,\"RedisPassword\": \"Hzss@123\"}", + "CONNECTIONSTRING": "{\"Server\": \"10.130.0.19\", \"Port\": 2415,\"Database\":\"pm-dev\",\"UserId\": \"ss\",\"Password\":\"Hzss@123\"}", "MASA_STACK": "[{\"id\":\"pm\",\"service\":{\"id\":\"pm-service-dev\",\"domain\":\"http://pm-service-dev.masastack.com\"},\"web\":{\"id\":\"pm-web-dev\",\"domain\":\"https://pm-dev.masastack.com\"}},{\"id\":\"dcc\",\"service\":{\"id\":\"dcc-service-dev\",\"domain\":\"http://dcc-service-dev.masastack.com\"},\"web\":{\"id\":\"dcc-web-dev\",\"domain\":\"https://dcc-dev.masastack.com\"}},{\"id\":\"tsc\",\"service\":{\"id\":\"tsc-service-dev\",\"domain\":\"http://tsc-service-dev.masastack.com\"},\"web\":{\"id\":\"tsc-web-dev\",\"domain\":\"https://tsc-dev.masastack.com\"}},{\"id\":\"alert\",\"service\":{\"id\":\"alert-service-dev\",\"domain\":\"http://alert-service-dev.masastack.com\"},\"web\":{\"id\":\"alert-web-dev\",\"domain\":\"https://alert-dev.masastack.com\"}},{\"id\":\"scheduler\",\"service\":{\"id\":\"scheduler-service-dev\",\"domain\":\"http://scheduler-service-dev.masastack.com\"},\"worker\":{\"id\":\"scheduler-worker-dev\",\"domain\":\"http://scheduler-worker-dev.masastack.com\"},\"web\":{\"id\":\"scheduler-web-dev\",\"domain\":\"https://scheduler-dev.masastack.com\"}},{\"id\":\"mc\",\"service\":{\"id\":\"mc-service-dev\",\"domain\":\"http://mc-service-dev.masastack.com\"},\"web\":{\"id\":\"mc-web-dev\",\"domain\":\"https://mc-dev.masastack.com\"}},{\"id\":\"auth\",\"service\":{\"id\":\"auth-service-dev\",\"domain\":\"http://auth-service-dev.masastack.com\"},\"web\":{\"id\":\"auth-web-dev\",\"domain\":\"https://auth-dev.masastack.com\"},\"sso\":{\"id\":\"auth-sso-dev\",\"domain\":\"https://auth-sso-dev.masastack.com\"}}]", "ELASTIC": "{\"Nodes\": [\"http://es-ydy-new.lonsid.cn:9200\"],\"Index\": \"auth_user_development\"}", "MASA_ENVIRONMENT": "Development", diff --git a/src/Web/Masa.Auth.Web.Admin.Server/appsettings.Development.json b/src/Web/Masa.Auth.Web.Admin.Server/appsettings.Development.json index 015d90b09..6a113e32e 100644 --- a/src/Web/Masa.Auth.Web.Admin.Server/appsettings.Development.json +++ b/src/Web/Masa.Auth.Web.Admin.Server/appsettings.Development.json @@ -11,12 +11,12 @@ "RedisOptions": { "Servers": [ { - "Host": "10.175.207.202", - "Port": "30260" + "Host": "10.130.0.19", + "Port": "2135" } ], - "DefaultDatabase": 0, - "Password": "Hzss@123redis" + "DefaultDatabase": 3, + "Password": "Hzss@123" } } } diff --git a/src/Web/Masa.Auth.Web.Sso/Infrastructure/Validations/ImpersonationCacheItem.cs b/src/Web/Masa.Auth.Web.Sso/Infrastructure/Validations/ImpersonationCacheItem.cs new file mode 100644 index 000000000..8aa97d0ff --- /dev/null +++ b/src/Web/Masa.Auth.Web.Sso/Infrastructure/Validations/ImpersonationCacheItem.cs @@ -0,0 +1,32 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Web.Sso.Infrastructure.Validations; + +[Serializable] +public class ImpersonationCacheItem +{ + public const string CACHE_NAME = "AppImpersonationCache"; + + public int? ImpersonatorTenantId { get; set; } + + public long ImpersonatorUserId { get; set; } + + public int? TargetTenantId { get; set; } + + public long TargetUserId { get; set; } + + public bool IsBackToImpersonator { get; set; } + + public ImpersonationCacheItem() + { + + } + + public ImpersonationCacheItem(int? targetTenantId, long targetUserId, bool isBackToImpersonator) + { + TargetTenantId = targetTenantId; + TargetUserId = targetUserId; + IsBackToImpersonator = isBackToImpersonator; + } +} \ No newline at end of file diff --git a/src/Web/Masa.Auth.Web.Sso/Infrastructure/Validations/ImpersonationGrantValidator.cs b/src/Web/Masa.Auth.Web.Sso/Infrastructure/Validations/ImpersonationGrantValidator.cs new file mode 100644 index 000000000..d84b0e911 --- /dev/null +++ b/src/Web/Masa.Auth.Web.Sso/Infrastructure/Validations/ImpersonationGrantValidator.cs @@ -0,0 +1,53 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the Apache License. See LICENSE.txt in the project root for license information. + +namespace Masa.Auth.Web.Sso.Infrastructure.Validations; + +public class ImpersonationGrantValidator : IExtensionGrantValidator +{ + IAuthClient _authClient; + public string GrantType { get; } = "impersonation"; + + const string IMPERSONATOR_USER_ID = "http://Lonsid.org/identity/claims/impersonatorUserId"; + + public ImpersonationGrantValidator(IAuthClient authClient) + { + _authClient = authClient; + } + + public async Task ValidateAsync(ExtensionGrantValidationContext context) + { + var impersonationToken = context.Request.Raw["impersonationToken"]; + if (string.IsNullOrEmpty(impersonationToken)) + { + context.Result = new GrantValidationResult + { + IsError = true, + Error = "Must provide impersonationToken", + ErrorDescription = "Must provide impersonationToken" + }; + return; + } + + var cacheItem = await _authClient.UserService.GetImpersonatedUserAsync(impersonationToken); + if (cacheItem is null) + { + context.Result = new GrantValidationResult + { + IsError = true, + Error = "Impersonated user does not exist", + ErrorDescription = "Impersonated user does not exist", + }; + return; + } + + var claims = new List(); + + if (!cacheItem.IsBackToImpersonator) + { + claims.Add(new Claim(IMPERSONATOR_USER_ID, cacheItem.ImpersonatorUserId.ToString())); + } + + context.Result = new GrantValidationResult(cacheItem.TargetUserId.ToString(), "impersonation", claims); + } +} diff --git a/src/Web/Masa.Auth.Web.Sso/Masa.Auth.Web.Sso.csproj b/src/Web/Masa.Auth.Web.Sso/Masa.Auth.Web.Sso.csproj index 942690175..39fc31c38 100644 --- a/src/Web/Masa.Auth.Web.Sso/Masa.Auth.Web.Sso.csproj +++ b/src/Web/Masa.Auth.Web.Sso/Masa.Auth.Web.Sso.csproj @@ -24,7 +24,7 @@ - +