On Kentico 12, the property Security inside the Page doesn't have Access field like the previous version Kentico 11 - Interface Access.
I need to provide this feature, so I was thinking about using overriding the OnAuthentication method like this:
protected override void OnAuthentication(AuthenticationContext filterContext)
{
var isAuthenticated = filterContext.Principal.Identity.IsAuthenticated;
var routePath = filterContext.HttpContext.Request.Path;
var page = DocumentHelper.GetDocuments().Path(routePath).FirstOrDefault();
var allowAccess = (page.HasSecureProperty && isAuthenticated) || !page.HasSecureProperty;
if (allowAccess)
{
base.OnAuthentication(filterContext);
}
else
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(new { controller = "Account", action = "Signin" })
);
}
}
HasSecureProperty would be the property from the kentico page that admins or editors users can set on the administration panel. I was planning to create this property using custom table and make a interface on the page for the users.
The field IsSecureNode on CMS_Tree seems to be the property that I need and was been used on previous versions, but I couldn't find a way to set on the new admin panel.
Is there another solution to allow users to set authentication on pages? I was concerned about performance since this method will be called on every action. Thank you.
I have done something similar so maybe it will help point you in the right direction.
My entire MVC site requires authentication, so that might be where this differs. In the MVC when I want to get files and check permissions I do something like this:
var files = subcat.Children.WithAllData.WithPermissionsCheck;
On the CMS side, I have a field on the page type that allows a user to select roles and another one for selecting users. I then have a custom event on the document update or insert to update the settings.
Here is the code I use for updating the ACLs:
private void UpdateSettings(TreeNode node)
{
ObjectQuery<RoleInfo> roles = null;
ObjectQuery<UserInfo> users = null;
var columnRoles = node.GetStringValue("Roles", "");
if (columnRoles != "")
{
var rolesConcat = columnRoles.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
var where = "RoleName IN " + "('" + string.Join("','", rolesConcat) + "')";
EventLogProvider.LogInformation("Document Event", "Roles", where);
roles = RoleInfoProvider.GetRoles()
.Where(where);
}
var columnUsers = node.GetStringValue("Users", "");
if (columnUsers != "")
{
var usersConcat = columnUsers.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
var where = "UserName IN " + "('" + string.Join("','", usersConcat) + "')";
EventLogProvider.LogInformation("Document Event", "Users", where);
users = UserInfoProvider.GetUsers()
.Where(where);
}
if (node != null)
{
// Gets the ID of the ACL item that stores the page's permission settings
int nodeACLID = ValidationHelper.GetInteger(node.GetValue("NodeACLID"), 0);
// Deletes the page's ACL item
// Removes the page's permission settings for all users and roles
AclItemInfoProvider.DeleteAclItems(nodeACLID);
node.IsSecuredNode = true;
int allowed = DocumentSecurityHelper.GetNodePermissionFlags(NodePermissionsEnum.Read);
// Prepares a value indicating that no page permissions are denied
int denied = 0;
if (users != null)
foreach (var user in users)
{
// Sets the page's permission for the user (allows the 'Modify' permission)
AclItemInfoProvider.SetUserPermissions(node, allowed, denied, user);
}
if (roles != null)
foreach (var role in roles)
{
// Sets the page's permission for the user (allows the 'Modify' permission)
AclItemInfoProvider.SetRolePermissions(node, allowed, denied, role);
}
}
}
You can use the approach mentioned in the documentation on authorizing live site actions.
I ended up using a custom table with an interface for the user to set if the page requires authentication or not. Since this is an override of OnAuthentication, every page calls this method. I hope there is a better solution using built-in Kentico features. Here is the final code:
protected override void OnAuthentication(AuthenticationContext filterContext)
{
base.OnAuthentication(filterContext);
var routePath = filterContext.HttpContext.Request.Path;
var allowAccess = Authentication.CanEnterPage(filterContext.Principal.Identity.IsAuthenticated, routePath);
if (!allowAccess)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(new { controller = "Account", action = "Signin", returnUrl = routePath })
);
}
}
The static method below contains the logic to access the page:
public static bool CanEnterPage(bool isAuthenticated, string routePath)
{
var page = DocumentHelper.GetDocuments().Path(routePath).FirstOrDefault();
if (page == null)
return false;
var pageAccess = PageAccessInfoProvider.GetPageAccesses()
.WhereEquals("PageAccessNodeID", page.NodeID).FirstOrDefault();
// Create a record if pageAccess is null
if (pageAccess == null)
{
pageAccess = CreateRecordsPageAccess(page);
}
var isSecure = pageAccess.PageAccessHasAuthentication;
var allowAccess = isSecure && isAuthenticated || !isSecure;
return allowAccess;
}
Related
New to Blazor and have been doing a hatchet job to get things working how I want.
I am using Blazor WASM with AAD for Authentication created based on this document MS Doc. I implemented the SecureAccountFactory class from the example and call a db where I get the associated user based on the AAD Guid, then add everything into Claims.
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(SecureUserAccount account,
RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
var userIdentity = (ClaimsIdentity)initialUser.Identity;
var claims = userIdentity.Claims;
var principalId = claims.Where(x => x.Type == "oid").First();
//Get some user info from SQL
var User = await _UserService.Get(principalId.Value);
//Get user Roles from SQL and add to Claims
var UsersInRoles = await _UsersInRoleService.RolesByUserId(principalId.Value);
//Add the ClientId to Claims
userIdentity.AddClaim(new Claim("clientId", User.ClientId.ToString()));
foreach (var userrole in UsersInRoles)
{
userIdentity.AddClaim(new Claim("appRole", userrole.Role.Name));
}
}
return initialUser;
}
I then have a Profile Component that appears on every page as part of the MainLayout which should have some info about the current user, so I made a static class to retrieve this info.
public static class UserHelper
{
public static async Task<CurrentUserClaims> GetCurrentUserClaims(Task<AuthenticationState> authenticationStateTask)
{
AuthenticationState authenticationState;
authenticationState = await authenticationStateTask;
var AuthenticationStateUser = authenticationState.User;
var user = authenticationState.User;
var claims = user.Claims;
var clientClaim = claims.Where(x => x.Type == "clientId").First();
var principalId = claims.Where(x => x.Type == "oid").First();
return new CurrentUserClaims
{
ClientId = Convert.ToInt32(clientClaim.Value),
PrincipalId = Guid.Parse(principalId.Value),
user = user
};
}
}
In my ProfileComponent, I call CascadingParameter and then onParametersSet I query my Static class for the info from the current logged in user
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private string profilePath;
protected override async Task OnParametersSetAsync()
{
CurrentUserClaims UserClaims = await UserHelper.GetCurrentUserClaims(authenticationStateTask);
var principal = UserClaims.PrincipalId;
//... do stuff
}
The above all works, after a Refresh or once I route to any other page. The initial Load, after login on the home page shows that the below line always fails with 'Sequence contains no elements'
var clientClaim = claims.Where(x => x.Type == "clientId").First();
I am using Authorize to protect the pages and I will eventually be using the Roles to determine what to display to the user.
A: Surely there's a better way of doing the above. There are lots and lots of articles on creating a custom Auth which inherits AuthenticationState but every one I've seen adds the Claims manually as a fake user, so I don't see how to access the actual Claims.
B: I'm wondering if just using LocalStorage for the User info might be a simpler way to go but is it considered 'safe' or best practice?
Any pointers to a solution are appreciated.
I am using identity core for user management in .net core 3.1 web api. Now, I want to check the users email for something and if it meets the requirement only then he will be created. The code below tells a lot about what I want to achieve
I have a custom user validator as below:
public class CustomEmailValidator<TUser> : IUserValidator<TUser>
where TUser : User
{
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user)
{
User userFromEmail = null;
if(!string.IsNullOrEmpty(user.Email))
userFromEmail = manager.FindByEmailAsync(user.Email).Result;
if (userFromEmail == null)
return Task.FromResult(IdentityResult.Success);
return Task.FromResult(
IdentityResult.Failed(new IdentityError
{
Code = "Err",
Description = "You are already registered with us."
}));
}
}
I add the validator in my startup as below:
services.AddDbContext<DataContext>(x => x.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.User.RequireUniqueEmail = false;
opt.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789._-";
opt.Password.RequireDigit = true;
opt.Password.RequiredLength = 6;
opt.Password.RequireNonAlphanumeric = true;
opt.Password.RequireUppercase = true;
opt.Password.RequireLowercase = true;
})
.AddUserValidator<CustomEmailValidator<User>>();
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<DataContext>();
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
As can be seen, I want to use the default user validation and my custom validation too. The problem being the user gets created right after the default validation and the email always turns out as exists in my custom validation. I don't really want to override my default validations.
Creating the user as below:
[HttpPost("Register")]
public async Task<IActionResult> Register(UserForRegisterDto userForRegister)
{
var userToCreate = _mapper.Map<User>(userForRegister);
var result = await _userManager.CreateAsync(userToCreate, userForRegister.Password);
if (result.Succeeded)
{
var roleresult = await _userManager.AddToRoleAsync(userToCreate, "Member");
return Ok(roleresult);
}
return BadRequest(result.Errors);
}
Note This is not my actual use case. I know I can check for unique email in my default validation by making opt.User.RequireUniqueEmail = true. This is just to clear a concept for further development.
Update After further debugging, I see that the custom validation method is called twice. Once before user creation and once after creation for some reason. I insert a new unique email and the custom validation passes success and after user creation, custom validation is called again and find the email registered already and throws an error message. This is weird
Found out that AddToRoleAsync was calling the custom validator again and was finding the user present in the table. Had to include a check whether the user found in the table with the same email is the same as user getting getting updated.
Code below:
public class CustomEmailValidator<TUser> : IUserValidator<TUser>
where TUser : User
{
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user)
{
User userFromEmail = null;
if(!string.IsNullOrEmpty(user.Email))
userFromEmail = manager.FindByEmailAsync(user.Email).Result;
if (userFromEmail == null)
return Task.FromResult(IdentityResult.Success);
else {
if(string.Equals(userFromEmail.Id, user.Id))
{
return Task.FromResult(IdentityResult.Success);
}
}
return Task.FromResult(
IdentityResult.Failed(new IdentityError
{
Code = "Err",
Description = "You are already registered with us."
}));
}
}
This should help a lot of people
A user in my web application may have several roles coming from different applications. Those roles are stored in the HttpContext in an array that is Serialized called Roles.
Currently for my controller I implement a custom filter that deserializes the array and reads the items within like this
public void OnActionExecuting(ActionExecutingContext context)
{
string[] applications = ListOfApplications.Split(",");
string[] roles = ListOfRoles.Split(",");
var userRoles = context.HttpContext.User.Claims.Where(c => c.Type == "Roles").Select(c => c.Value).ToList();
var matches = 0;
foreach (var item in userRoles)
{
var currentItem = JsonConvert.DeserializeObject<UserRoleDetailsViewModel>(item);
UserRoleDetailsViewModel urdvm = new UserRoleDetailsViewModel
{
Id = currentItem.Id,
Name = currentItem.Name,
ApplicationId = currentItem.ApplicationId,
ApplicationName = currentItem.ApplicationName
};
for (var i = 0; i < applications.Length; i++)
{
if(applications[i] == ApplicationGlobals.All && roles[i] == RoleGlobals.All)
{
matches++;
}
if(applications[i]== ApplicationGlobals.All && roles[i] == urdvm.Name)
{
matches++;
}
if(applications[i] == urdvm.ApplicationName && roles[i] == urdvm.Name)
{
matches++;
}
}
}
if (matches == 0)
{
context.Result = new RedirectToActionResult("Index", "Home", null);
}
}
And I call it on top of my controller like this:
[Authorize]
[TypeFilter(typeof(ValidateRolesFilter), Arguments = new object[] {
ApplicationGlobals.app1+","+ApplicationGlobals.app1
,RoleGlobals.SystemAdministrator+","+RoleGlobals.User
})]
However, this only works until the controller. How can I extend this to work in a view so that I am able to segment off areas in the navigation bar for example based on the users role? Is this possible?
However, this only works until the controller. How can I extend this to work in a view so that I am able to segment off areas in the navigation bar for example based on the users role? Is this possible?
Yes! possible. Following is the way of doing so in ASP.NET Core identity.
#using Microsoft.AspNetCore.Identity
#inject SignInManager<ApplicationUser> SignInManager // ApplicatonUser is the class that inherited IndentityUser
#inject UserManager<ApplicationUser> UserManager
<ul class="navbar-nav">
#if (SignInManager.IsSignedIn(User))
{
// Here is navbar items for authenticated (logged in) user
If(User.IsInRole("Admin");)
{
// Here is navbar items for only user with `Admin` Role
}
}
else
{
// Here is navbar items for user if not logged in
}
</ul>
I am writing an application where the users are sharing specific files via Dropbox. I am using the REST API. Lets say A shares a file to B. I know that A can share the file by adding B as a member to a file(add_file_member) and that B can check files shared with him by list_received_files. But I can't find a way that B can see who shared the file. It seems reasonable to have access to this info. Is there a way to obtain some account id or a display info for the user A from B perspective?
The SharedFileMetadata object returned by /sharing/list_received_files doesn't include this information, but I'll be sure to pass this along as a feature request.
However, you can use /sharing/list_file_members to list the members of the shared file, as well as their access level (e.g., owner, editor, etc.).
Example for C# Dropbox SDK.
public async Task<string> GetSharedFileOwnerID(Dropbox.Api.Files.Metadata data, Dropbox.Api.DropboxClient DropboxClient)
{
string owner = "";
var file = data.AsFile;
if ((file.HasExplicitSharedMembers ?? false) || (file.SharingInfo != null))
{
// search the real file owner
var list = await DropboxClient.Sharing.ListFileMembersAsync(file.Id);
var list_continue = false;
do
{
var item = list.Users.First(i => i.AccessType.IsOwner);
if (item != null)
{
owner = item.User.AccountId;
break;
}
list_continue = (list.Cursor != null);
if (list_continue)
{
list = await DropboxClient.Sharing.ListFileMembersContinueAsync(list.Cursor);
}
}
while (list_continue);
}
return owner;
}
public async Task<string> GetSharedFoldeOwnerID(Dropbox.Api.Files.Metadata data, Dropbox.Api.DropboxClient DropboxClient)
{
string owner = "";
var folder = data.AsFolder;
if (folder.SharedFolderId != null)
{
// search the real folder owner
var list = await DropboxClient.Sharing.ListFolderMembersAsync(folder.SharedFolderId);
var list_continue = false;
do
{
var item = list.Users.First(i => i.AccessType.IsOwner);
if (item != null)
{
owner = item.User.AccountId;
break;
}
list_continue = (list.Cursor != null);
if (list_continue)
{
list = await DropboxClient.Sharing.ListFolderMembersContinueAsync(list.Cursor);
}
}
while (list_continue);
}
return owner;
}
C# Dropbox SDK is NuGet package.
Use Install-Package Dropbox.Api for install it.
I have a entity record which is shared with or more users. I would like to unshare this record when Deactivate it. I want to do that in Plugin. But I can't understand how to get all users from sharing list who have access to this record. How to do that?
Here is my code snippet:
protected void ExecutePostPersonSetStateDynamicEntity(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
var context = localContext.PluginExecutionContext;
var targetEntity = (Entity)context.InputParameters["EntityMoniker"];
var state = (OptionSetValue)context.InputParameters["State"];
var columns = new ColumnSet(new[] { "statecode" });
var retrivedEntity = localContext.OrganizationService.Retrieve(targetEntity.LogicalName, targetEntity.Id, columns);
if (state.Value == 1)
{
RevokeAccessRequest revokeRequest = new RevokeAccessRequest()
{
Target = new EntityReference(personEntity.LogicalName, personEntity.Id),
Revokee = new EntityReference(neededEntity.LogicalName, needed.Id)
};
// Execute the request.
}
}
As you can see, I need an entity "neededEntity", I don't know how to get it from "targetEntity" or "retrievedEntity".
You need to use a RetrieveSharedPrincipalsAndAccessRequest
http://msdn.microsoft.com/en-us/library/microsoft.crm.sdk.messages.retrievesharedprincipalsandaccessrequest.aspx
You can start from the included example, basically inside the foreach you call your RevokeAcessRequest