Files
Umbraco-CMS/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
Elitsa Marinovska fda866fc9e V14: Add authorization policies to Management API controllers - p2 (#15211)
* Making ProblemDetails details more generic

* Adding authorizer that can be replaces for external authz in handlers. Adding handler and requirement for UserBelongsToUserGroupInRequest policy

* Adding method to get the GUID from claims

* Adding service methods to check user group authz

* Porting MustSatisfyRequirementAuthorizationHandler

* Adding controllers authz

* Fix return status code + produced response type

* Moving to folder

* Adding DenyLocalLogin policy scaffold

* Implement a temp DenyLocalLoginHandler

* Introducing a new Fobidden result

* Fix comment

* Introducing a helper class for authorizers

* Changed nullability for GetCurrentUser

* Changes from Attempt to Status + FIXME comments

* Create a UserGroupAuthorizationStatus to be used in the future

* Introduces a new authz status for checking media acess

* Introducing a new permission service for media

* Adding fixme

* Adding more policy configurations

* Adding Media policy requirement and handler

* Adding media authorizer

* Fix order of params

* Adding duplicate code comment

* Adding authz to media controllers

* Migrating more logic from MediaPermissions.cs

* Adding more MediaAuthorizationStatus-es

* Handling of new authorization status

* Fix comment

* Adding NotFound case

* Adding NewDenyLocalLoginIfConfigured policy && commenting [AllowAnonymous] where the policy is applied since it is already handled

* Changed Forbid() to Forbidden() to get the correct status code

* Remove policy that is applied on the base controller already

* Implement and apply NewUmbracoFeatureEnabled policy

* Renaming classes to add Permission in the name

* Register permission services

* Add FIXME

* Introduce new IUserGroupPermissionService and refactor accordingly

* Add single overload with default implementation

* Adding user permission policy and related

* Applying admin policy

* Register all new policies

* Better wording

* Add default implementation for a single overload

* Adding remarks to IContentPermissionService.cs

* Supporting null as key in ContentPermissionService

* Fix namespace

* Reverting back to not supporting null as content key, but having dedicated implementation

* Adding content authorizer with null values to represent root item

* Removing null key support and adding dedicated implementation

* Removing remarks

* Adding content resource with null support

* Removing null support

* Adding requirement and status

* Adding content authorizer + handlers

* Applying policies to content controllers

* Update comment

* Handling of Authorization Statuses

* More authz in controllers

* Fix comments

* New branch handler

* Obsolete old implementation

* Adding dedicated policies to root and bin

* Adding a branch specific namespace

* Bin specific requirement and namespace

* Root specific requirement and namespace

* Changing to new root policy

* Refactoring

* Save policies

* Fix null check/reference

* Add TODO comment

* Create media root- and bin-specific policies, handlers, etc.

* Apply correct policy in create and update media controllers

* Apply root policy to move and sort controllers

* Fix wording

* Adding UserGroupAuthorizationStatusResult

* Remove all AuthorizationStatusResult as we cannot get the specific AuthorizationStatus

* Fixing Umbraco feature policy

* Fix allow anonymous endpoints - the value returned from DenyLocalLoginHandler wasn't enough, we need to succeed DenyAnonymousAuthorizationRequirement as it is required for some of the endpoints that had the attribute

* Apply DenyLocalLoginIfConfigured policy to corresponding re-implementation of PostSetInvitedUserPassword

* Fix comment

* Renaming performingUser to user and fixing comments

* Rename helper method

* Fix references

* Re-add merge conflict deletion

* Adding Backoffice requirement and relevant

* Registering

* Added a simple policy test

* Fixed small test things and clean up

* Temp solution

* Added one more test and fix another static issue

* Fix another merge conflict

* Remove BackOfficePermissionRequirement and handler as they might not be necessary

* Comment out again [AllowAnonymous]

* Remove AuthorizationPolicies.BackOfficeAccessWithoutApproval policy as it might not be necessary

* Fix temp implementation

* Fix reference to correct handler

* Apply authz policy to new publish/unpublish controllers

* Fix comments

* Removing duplicate ProducesResponseTypes

* Added swagger documentation about the 401 and 403

* Added Resources to Media, User and UserGroup

* Handle root, recycle bin and branch in the same handler

* Handle both parent and target when moving

* Check Ids for all sort requests

* Xml docs

* Clean up

* Clean up

* Fix build

* Cleanup

* Remove TODO

* Added missing overload

* Use yield

* Adding some keys to check

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2023-12-11 08:25:29 +01:00

421 lines
15 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security.Claims;
using System.Security.Principal;
using Umbraco.Cms.Core;
namespace Umbraco.Extensions;
public static class ClaimsIdentityExtensions
{
/// <summary>
/// Returns the required claim types for a back office identity
/// </summary>
/// <remarks>
/// This does not include the role claim type or allowed apps type since that is a collection and in theory could be
/// empty
/// </remarks>
public static IEnumerable<string> RequiredBackOfficeClaimTypes => new[]
{
ClaimTypes.NameIdentifier, // id
ClaimTypes.Name, // username
ClaimTypes.GivenName,
// Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null...
// Constants.Security.StartMediaNodeIdClaimType,
ClaimTypes.Locality, Constants.Security.SecurityStampClaimType,
};
public static T? GetUserId<T>(this IIdentity identity)
{
var strId = identity.GetUserId();
Attempt<T> converted = strId.TryConvertTo<T>();
return converted.Result ?? default;
}
/// <summary>
/// Returns the user id from the <see cref="IIdentity" /> of either the claim type
/// <see cref="ClaimTypes.NameIdentifier" /> or "sub"
/// </summary>
/// <param name="identity"></param>
/// <returns>
/// The string value of the user id if found otherwise null
/// </returns>
public static string? GetUserId(this IIdentity identity)
{
if (identity == null)
{
throw new ArgumentNullException(nameof(identity));
}
string? userId = null;
if (identity is ClaimsIdentity claimsIdentity)
{
userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier)
?? claimsIdentity.FindFirstValue("sub");
}
return userId;
}
/// <summary>
/// Returns the user key from the <see cref="IIdentity" /> of the claim type "sub".
/// </summary>
/// <param name="identity"></param>
/// <returns>
/// The string value of the user id if found otherwise null.
/// </returns>
public static Guid? GetUserKey(this IIdentity identity)
{
if (identity is null)
{
throw new ArgumentNullException(nameof(identity));
}
string? userKey = null;
if (identity is ClaimsIdentity claimsIdentity)
{
userKey = claimsIdentity.FindFirstValue("sub");
}
return Guid.TryParse(userKey, out Guid result)
? result
: null;
}
/// <summary>
/// Returns the user name from the <see cref="IIdentity" /> of either the claim type <see cref="ClaimTypes.Name" /> or
/// "preferred_username"
/// </summary>
/// <param name="identity"></param>
/// <returns>
/// The string value of the user name if found otherwise null
/// </returns>
public static string? GetUserName(this IIdentity identity)
{
if (identity == null)
{
throw new ArgumentNullException(nameof(identity));
}
string? username = null;
if (identity is ClaimsIdentity claimsIdentity)
{
username = claimsIdentity.FindFirstValue(ClaimTypes.Name)
?? claimsIdentity.FindFirstValue("preferred_username");
}
return username;
}
public static string? GetEmail(this IIdentity identity)
{
if (identity == null)
{
throw new ArgumentNullException(nameof(identity));
}
string? email = null;
if (identity is ClaimsIdentity claimsIdentity)
{
email = claimsIdentity.FindFirstValue(ClaimTypes.Email);
}
return email;
}
/// <summary>
/// Returns the first claim value found in the <see cref="ClaimsIdentity" /> for the given claimType
/// </summary>
/// <param name="identity"></param>
/// <param name="claimType"></param>
/// <returns>
/// The string value of the claim if found otherwise null
/// </returns>
public static string? FindFirstValue(this ClaimsIdentity identity, string claimType)
{
if (identity == null)
{
throw new ArgumentNullException(nameof(identity));
}
return identity.FindFirst(claimType)?.Value;
}
/// <summary>
/// Verify that a ClaimsIdentity has all the required claim types
/// </summary>
/// <param name="identity"></param>
/// <param name="verifiedIdentity">Verified identity wrapped in a ClaimsIdentity with BackOfficeAuthentication type</param>
/// <returns>True if ClaimsIdentity</returns>
public static bool VerifyBackOfficeIdentity(
this ClaimsIdentity identity,
[MaybeNullWhen(false)] out ClaimsIdentity verifiedIdentity)
{
if (identity is null)
{
verifiedIdentity = null;
return false;
}
// Validate that all required claims exist
foreach (var claimType in RequiredBackOfficeClaimTypes)
{
if (identity.HasClaim(x => x.Type == claimType) == false ||
identity.HasClaim(x => x.Type == claimType && x.Value.IsNullOrWhiteSpace()))
{
verifiedIdentity = null;
return false;
}
}
verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType
? identity
: new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
return true;
}
/// <summary>
/// Add the required claims to be a BackOffice ClaimsIdentity
/// </summary>
/// <param name="identity">this</param>
/// <param name="userId">The users Id</param>
/// <param name="username">Username</param>
/// <param name="realName">Real name</param>
/// <param name="startContentNodes">Start content nodes</param>
/// <param name="startMediaNodes">Start media nodes</param>
/// <param name="culture">The locality of the user</param>
/// <param name="securityStamp">Security stamp</param>
/// <param name="allowedApps">Allowed apps</param>
/// <param name="roles">Roles</param>
public static void AddRequiredClaims(this ClaimsIdentity identity, string userId, string username, string realName, IEnumerable<int>? startContentNodes, IEnumerable<int>? startMediaNodes, string culture, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
{
// This is the id that 'identity' uses to check for the user id
if (identity.HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false)
{
identity.AddClaim(new Claim(
ClaimTypes.NameIdentifier,
userId,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == false)
{
identity.AddClaim(new Claim(
ClaimTypes.Name,
username,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false)
{
identity.AddClaim(new Claim(
ClaimTypes.GivenName,
realName,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false &&
startContentNodes != null)
{
foreach (var startContentNode in startContentNodes)
{
identity.AddClaim(new Claim(
Constants.Security.StartContentNodeIdClaimType,
startContentNode.ToInvariantString(),
ClaimValueTypes.Integer32,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
}
if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false &&
startMediaNodes != null)
{
foreach (var startMediaNode in startMediaNodes)
{
identity.AddClaim(new Claim(
Constants.Security.StartMediaNodeIdClaimType,
startMediaNode.ToInvariantString(),
ClaimValueTypes.Integer32,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
}
if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false)
{
identity.AddClaim(new Claim(
ClaimTypes.Locality,
culture,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
// The security stamp claim is also required
if (identity.HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false)
{
identity.AddClaim(new Claim(
Constants.Security.SecurityStampClaimType,
securityStamp,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
// Add each app as a separate claim
if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null)
{
foreach (var application in allowedApps)
{
identity.AddClaim(new Claim(
Constants.Security.AllowedApplicationsClaimType,
application,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
}
// Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might
// not be made with that factory if it was created with a different ticket so perform the check
if (identity.HasClaim(x => x.Type == ClaimsIdentity.DefaultRoleClaimType) == false && roles != null)
{
// Manually add them
foreach (var roleName in roles)
{
identity.AddClaim(new Claim(
identity.RoleClaimType,
roleName,
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
identity));
}
}
}
/// <summary>
/// Get the start content nodes from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Array of start content nodes</returns>
public static int[] GetStartContentNodes(this ClaimsIdentity identity) =>
identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType)
.Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
? i
: default)
.Where(x => x != default).ToArray();
/// <summary>
/// Get the start media nodes from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Array of start media nodes</returns>
public static int[] GetStartMediaNodes(this ClaimsIdentity identity) =>
identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType)
.Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
? i
: default)
.Where(x => x != default).ToArray();
/// <summary>
/// Get the allowed applications from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns></returns>
public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity
.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray();
/// <summary>
/// Get the user ID from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>User ID as integer</returns>
public static int? GetId(this ClaimsIdentity identity)
{
var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier);
if (firstValue is not null)
{
return int.Parse(firstValue, CultureInfo.InvariantCulture);
}
return null;
}
/// <summary>
/// Get the real name belonging to the user from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Real name of the user</returns>
public static string? GetRealName(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.GivenName);
/// <summary>
/// Get the username of the user from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Username of the user</returns>
public static string? GetUsername(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Name);
/// <summary>
/// Get the culture string from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Culture string</returns>
public static string? GetCultureString(this ClaimsIdentity identity) =>
identity.FindFirstValue(ClaimTypes.Locality);
/// <summary>
/// Get the security stamp from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Security stamp</returns>
public static string? GetSecurityStamp(this ClaimsIdentity identity) =>
identity.FindFirstValue(Constants.Security.SecurityStampClaimType);
/// <summary>
/// Get the roles assigned to a user from a ClaimsIdentity
/// </summary>
/// <param name="identity"></param>
/// <returns>Array of roles</returns>
public static string[] GetRoles(this ClaimsIdentity identity) => identity
.FindAll(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(role => role.Value).ToArray();
/// <summary>
/// Adds or updates and existing claim.
/// </summary>
public static void AddOrUpdateClaim(this ClaimsIdentity identity, Claim? claim)
{
if (identity == null)
{
throw new ArgumentNullException(nameof(identity));
}
if (claim is not null)
{
Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type);
identity.TryRemoveClaim(existingClaim);
identity.AddClaim(claim);
}
}
}