Skip to main content

Identity Manager - Document authorization

Overview

Document Permisions represents a mechanism to manage access to documents (i.e. records, instances) of specified entity. Permissions are defined by user roles and their permissions. Current user is represented by set of roles, if any of these roles fits to any of roles assigned to documents then functionality allows to manipulate with such document. Of course requested permission must fit to permissions assigned to appropriate roles.

Unlike normal permission tests for roles and rights objects, permission settings for individual documents are stored with the affected entity. Only the list of currently valid user roles is taken from IDM. Per-document accessibility testing is then performed directly on the service as part of the repository implementation.

Platform nugets

  • ASOL.IdentityManager.Authorization - Basic Authorization support.
  • ASOL.IdentityManager.Authorization.ConnectorBase - Authorization support for connector clients implementation.
  • ASOL.IdentityManager.Authorization.MongoDb - Document authorization support for Mongo.

Usage

Examples: Usage of document permissions in command/query handlers

using ASOL.Core.Identity.Authorization;

//check document permissions for document to retrieve
await repository.CheckPermissionsAsync(documentInstance, query.User, Permission.Read, ct);

//apply document permission filter to documents being retrieved
var filteredQuery = await repository.ApplyPermissionsAsync(documentQuery, query.User, Permission.Read, ct);

//check document permissions before update
await repository.CheckPermissionsAsync(_instance, command.User, Permission.Update, ct);

//check document permissions before delete
await repository.CheckPermissionsAsync(_instance, command.User, Permission.Delete, ct);

Implementation

Implementation consists of several parts:

  • domain entity object implements IProtectedEntity<TKey> extension interface
  • repository interface of domain entity using document permissions
  • MongoDB repository implementation of domain entity using document permissions
  • document permission facade of domain entity inherits DocumentPermissionFacadeBase<TEntity> base class
  • REST API controller of domain entity implements CRUD endpoints using IDocumentPermissionFacade<TEntity> interface
  • REST API client implements property of domain entity accessor using IDocumentPermissionClientAccessor interface and DocumentPermissionClientAccessor class

Implementation of domain entity object using document permissions

using ASOL.IdentityManager.Authorization;

/// <summary>
/// Represents a «MyEntity» domain entity.
/// </summary>
public class «MyEntity» : BaseEntity<string>, IProtectedEntity<string>
{
// ... initilization ...

// ... entity properties ...

string IProtectedEntity.DocumentId => Id;

public bool Protected { get; protected set; }

public IList<DocumentPermission> Permissions { get; protected set; } //unbound property (joined from document permission table of given entity)

public bool MarkAsProtected(ClaimsPrincipal user, IList<DocumentPermission> permissions = null)
{
if (Protected)
{
return false;
}

Protected = true;
Permissions = permissions ?? Array.Empty<DocumentPermission>();

return true;
}

public bool MarkAsUnprotected(ClaimsPrincipal user)
{
if (!Protected)
{
return false;
}

Protected = false;
Permissions = null;

return true;
}
}

Implementation of repository interface to domain entity using document permissions

using using ASOL.Core.Identity.Authorization;

/// <summary>
/// Represents a read/write set of repository operations for <see cref="«MyEntity»"/> entity.
/// </summary>
public interface I«MyEntity»Repository : I«MyEntity»ReadOnlyRepository, IRepository<«MyEntity», string>
{
void UpdateProtectedFlag(string id, bool protectedFlag);
}

/// <summary>
/// Represents a readonly set of repository operations for <see cref="«MyEntity»"/> entity.
/// </summary>
public interface I«MyEntity»ReadOnlyRepository : IReadOnlyRepository<«MyEntity», string>
{
Task CheckPermissionsAsync(«MyEntity» instance, ClaimsPrincipal user, Permission permission, CancellationToken ct = default);
Task<bool> HasPermissionsAsync(«MyEntity» instance, ClaimsPrincipal user, Permission permission, CancellationToken ct = default);
Task<IQueryable<«MyEntity»>> ApplyPermissionsAsync(IQueryable<«MyEntity»> query, ClaimsPrincipal user, Permission permission, CancellationToken ct = default);
}

Implementation of MongoDB repository to domain entity using document permissions

using ASOL.Core.Identity.Authorization;
using ASOL.IdentityManager.Authorization;
using ASOL.IdentityManager.Authorization.Exceptions;
using ASOL.IdentityManager.Authorization.MongoDb.Repositories;
using ASOL.IdentityManager.Authorization.Repositories;
using MongoDB.Driver;

public class «MyEntity»Repository : MongoProtectedRepository<«MyEntity», string>, I«MyEntity»Repository
{
///<summary>
/// Tenant Db .ctor
///</summary>
public «MyEntity»Repository(ITenantDbContext dbContext,
IPlatformAuthorizationService authorizationService,
IDbScopeSelector<IDocumentPermissionRepository<«MyEntity»>> permissionRepository)
: base(dbContext, authorizationService, permissionRepository)
{
}

///<summary>
/// Master Db .ctor
///</summary>
public «MyEntity»Repository(IDbContext dbContext,
IPlatformAuthorizationService authorizationService,
IDbScopeSelector<IDocumentPermissionRepository<«MyEntity»>> permissionRepository)
: base(dbContext, authorizationService, permissionRepository)
{
}

/// <inheritdoc/>
public override bool Delete(string id)
{
var deleted = base.Delete(id);
if (deleted)
{
//delete document permissions
var permissionRepository = PermissionRepository.GetRepository(UseMasterDbScope);
_ = permissionRepository.Delete(id);
}
return deleted;
}

/// <inheritdoc/>
public override async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
{
var deleted = await base.DeleteAsync(id, ct);
if (deleted)
{
//delete document permissions
var permissionRepository = PermissionRepository.GetRepository(UseMasterDbScope);
_ = await permissionRepository.DeleteAsync(id, ct);
}
return deleted;
}

/// <inheritdoc/>
public override Task<bool> DeleteAsync(Expression<Func<«MyEntity», bool>> predicate, CancellationToken ct = default)
{
throw new NotSupportedException();
}

/// <inheritdoc/>
public override bool Delete(FilterDefinition<«MyEntity»> filter, DeleteOptions options = null)
{
throw new NotSupportedException();
}

/// <inheritdoc/>
public override Task<bool> DeleteAsync(FilterDefinition<«MyEntity»> filter, DeleteOptions options = null, CancellationToken ct = default)
{
throw new NotSupportedException();
}

/// <inheritdoc/>
public void UpdateProtectedFlag(string id, bool protectedFlag)
{
var updateDefinition = Builders<«MyEntity»>.Update.Set(i => i.Protected, protectedFlag);
var collection = base.GetCollection();
collection.UpdateOne(GetByIdFilter(id), updateDefinition);
}

/// <inheritdoc/>
public override async Task CheckPermissionsAsync(«MyEntity» instance, ClaimsPrincipal user, Permission permission, CancellationToken ct = default)
{
var hasPerm = await HasPermissionsAsync(instance, user, permission, ct);
if (!hasPerm)
{
throw new UnauthorizedAccessByDocumentPermissionException(permission, instance.Id, nameof(Folder));
}
}

/// <inheritdoc/>
public async Task<IQueryable<«MyEntity»>> ApplyPermissionsAsync(IQueryable<«MyEntity»> query, ClaimsPrincipal user, Permission permission, CancellationToken ct = default)
{
//check if user is authenticated
ClaimsPrincipalExtensions.CheckAuthenticate(user);

if (UseMasterDbScope)
{
//space-owner has full access to any data in master dbscope
var isSpaceOwner = await AuthorizationService.IsSpaceOwnerRoleAsync(ct);
if (isSpaceOwner)
{
return query;
}
}
else
{
//tenant-owner has full access to any data in tenant dbscope
var isTenantOwner = await AuthorizationService.IsTenantOwnerRoleAsync(ct);
if (isTenantOwner)
{
return query;
}
}

//other-users have access to data based on roles and their permissions
var userRoles = await AuthorizationService.GetRolesAsync(ct);

//query entity documents and join with document permissions
var permissionCollection = GetDocumentPermissionCollection();
var resultQuery = from d in query
join d1 in GetCollection().AsQueryable()
on d.Id equals d1.Id into joinedDocs
join p in permissionCollection.AsQueryable()
on d.Id equals p.DocumentId into joinedPerms
where !d.Protected ||
joinedPerms.Any(j => j.Permissions.Contains(permission)
&& userRoles.Contains(j.RoleCode))
select joinedFolders;

return resultQuery.SelectMany(f => f);
}
}

Implementation of document permission facade to domain entity using document permissions

using ASOL.Core.Identity.Authorization;
using ASOL.IdentityManager.Authorization;
using ASOL.IdentityManager.Authorization.Repositories;

/// <inheritdoc cref="IDocumentPermissionFacade{TEntity}"/>
public class «MyEntity»DocumentPermissionFacade : DocumentPermissionFacadeBase<«MyEntity»>
{
protected IDbScopeSelector<I«MyEntity»Repository> Repository { get; }

public «MyEntity»DocumentPermissionFacade(
IDbScopeSelector<IDocumentPermissionRepository<«MyEntity»>> permissionRepository,
IDbScopeSelector<I«MyEntity»Repository> repository)
: base(permissionRepository)
{
Repository = repository;
}

protected override (IProtectedEntity Instance, string Id) CheckIfDocumentExistsAndGetInstance(DataAccessLevel accessLevel, string documentId)
{
var repository = Repository.GetRepository(accessLevel == DataAccessLevel.Public);
var instance = repository.GetById(documentId);
if (instance == null)
{
throw new KeyNotFoundException($"«MyEntity» '{documentId}' doesn't exist.");
}

return (instance, instance.Id);
}

protected override Task UpdateProtectedFlagOnDocumentAsync(DataAccessLevel accessLevel, (IProtectedEntity Instance, string Id) entity, CancellationToken ct)
{
var repository = Repository.GetRepository(accessLevel == DataAccessLevel.Public);
repository.UpdateProtectedFlag(entity.Id, entity.Instance.Protected);
return Task.CompletedTask;
}

public override async Task<bool> CheckDocumentPermissionAsync(ClaimsPrincipal user, DataAccessLevel accessLevel, string documentId, Permission permission, CancellationToken ct = default)
{
var repository = Repository.GetRepository(accessLevel == DataAccessLevel.Public);
var instance = repository.GetById(documentId);
if (instance == null)
{
throw new KeyNotFoundException($"«MyEntity» '{documentId}' doesn't exist.");
}

return await repository.HasPermissionsAsync(instance, user, permission, ct);
}
}

Implementation of domain entity REST API controller using document permissions

using ASOL.IdentityManager.Authorization;
using ASOL.IdentityManager.Primitives.Authorization;

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiConventionType(typeof(DefaultApiConventions))]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
public class «MyEntity»sController : AuthorizeControllerBase
{
public «MyEntity»sController(
ILogger<«MyEntity»Controller> logger,
IDocumentPermissionFacade<«MyEntity»> permissionFacade)
: base(logger)
{
PermissionFacade = permissionFacade;
}

protected IDocumentPermissionFacade<«MyEntity»> PermissionFacade { get; }

// ... api endpoints ...

#region document permissions

/// <summary>
/// Gets document permissions assigned to «MyEntity».
/// </summary>
/// <param name="id">The «MyEntity» identifier</param>
/// <param name="accessLevel">The data access level</param>
[HttpGet]
[Route("{id:guid}/DocumentPermissions")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CollectionResult<DocumentPermissionModel>))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetPermissions([FromRoute] string id, [FromQuery] DataAccessLevel accessLevel)
{
var result = await PermissionFacade.GetDocumentPermissionsAsync(User, accessLevel, id);
return Ok(result);
}

/// <summary>
/// Create document permissions assigned to «MyEntity».
/// </summary>
/// <param name="id">The «MyEntity» identifier</param>
/// <param name="modelCreate"></param>
/// <param name="accessLevel">The data access level</param>
[HttpPost]
[Route("{id:guid}/DocumentPermissions")]
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(CollectionResult<DocumentPermissionModel>))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CreatePermissions([FromRoute] string id, [FromBody] DocumentPermissionModelSet modelCreate, [FromQuery] DataAccessLevel accessLevel)
{
var created = await PermissionFacade.CreateDocumentPermissionsAsync(User, accessLevel, id, modelCreate);
var result = await PermissionFacade.GetDocumentPermissionsAsync(User, accessLevel, id);
return CreatedAtAction(nameof(GetPermissions), new
{
version = HttpContext.GetRequestedApiVersion().ToString(),
id,
accessLevel
},
result);
}

/// <summary>
/// Update document permissions assigned to «MyEntity».
/// </summary>
/// <param name="id">The «MyEntity» identifier</param>
/// <param name="modelUpdate"></param>
/// <param name="accessLevel">The data access level</param>
[HttpPut]
[Route("{id:guid}/DocumentPermissions")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CollectionResult<DocumentPermissionModel>))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdatePermissions([FromRoute] string id, [FromBody] DocumentPermissionModelSet modelUpdate, [FromQuery] DataAccessLevel accessLevel)
{
var updated = await PermissionFacade.SetDocumentPermissionsAsync(User, accessLevel, id, modelUpdate);
var result = await PermissionFacade.GetDocumentPermissionsAsync(User, accessLevel, id);
return Ok(result);
}

/// <summary>
/// Delete document permissions assigned to «MyEntity».
/// </summary>
/// <param name="id">The «MyEntity» identifier</param>
/// <param name="accessLevel">The data access level</param>
[HttpDelete]
[Route("{id:guid}/DocumentPermissions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeletePermissions([FromRoute] string id, [FromQuery] DataAccessLevel accessLevel)
{
var deleted = await PermissionFacade.DeleteDocumentPermissionsAsync(User, accessLevel, id);
return deleted ? (ActionResult)NoContent() : NotFound();
}

#endregion
}

Implementation of REST API client for domain entity accessor to document permissions

using ASOL.IdentityManager.Authorization.ConnectorBase;

/// <inheritdoc cref="I«MyService»Client"/>
public class «MyService»Client : SecuredApiClientBase<«MyService»ClientOptions, IConnectorTokenProvider>, I«MyService»Client
{
// ... initilization ...

public IDocumentPermissionClientAccessor «MyEntity»Permissions { get; }

/// .ctor
protected «MyService»Client(
«MyService»ClientOptions options,
ILogger<«MyService»Client> logger,
IConnectorTokenProvider tokenProvider,
IEnumerable<IRequestHeaderProvider> headerProviders,
HttpClient baseClient = null,
bool manageBaseClient = false)
: base(options, logger, tokenProvider, headerProviders, baseClient, manageBaseClient)
{
«MyEntity»Permissions = new DocumentPermissionClientAccessor($"{ApiVersionPrefix}/«MyEntity»s", Client, Logger, CombineUri, AddAuthentication);
}

// ... api methods ...
}