|
|
|
|
using Connected.Middleware;
|
|
|
|
|
using Connected.Security.Authorization.Middleware;
|
|
|
|
|
using Connected.Security.Permissions;
|
|
|
|
|
using Connected.Threading;
|
|
|
|
|
using System.Collections.Immutable;
|
|
|
|
|
|
|
|
|
|
namespace Connected.Security.Authorization;
|
|
|
|
|
|
|
|
|
|
internal class AuthorizationService : IAuthorizationService
|
|
|
|
|
{
|
|
|
|
|
public AuthorizationService(IMiddlewareService middleware, IPermissionService permissions)
|
|
|
|
|
{
|
|
|
|
|
Middleware = new AsyncLazy<ImmutableList<IAuthorizationMiddleware>>(middleware.Query<IAuthorizationMiddleware>());
|
|
|
|
|
Permissions = permissions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IPermissionService Permissions { get; }
|
|
|
|
|
|
|
|
|
|
private AsyncLazy<ImmutableList<IAuthorizationMiddleware>> Middleware { get; }
|
|
|
|
|
|
|
|
|
|
public async Task<IAuthorizationResult> Authorize(AuthorizationArgs args)
|
|
|
|
|
{
|
|
|
|
|
if (args is null)
|
|
|
|
|
throw new ArgumentException(null, nameof(args));
|
|
|
|
|
/*
|
|
|
|
|
* Claim is required otherwise we don't know what to authorize.
|
|
|
|
|
*/
|
|
|
|
|
if (string.IsNullOrWhiteSpace(args.Claim))
|
|
|
|
|
return AuthorizationResult.Fail(AuthorizationResultReason.NoClaim);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Query permissions for the specified arguments.
|
|
|
|
|
*/
|
|
|
|
|
var permissions = await Permissions.Query(new PermissionSearchArgs
|
|
|
|
|
{
|
|
|
|
|
Entity = args.Entity,
|
|
|
|
|
PrimaryKey = args.PrimaryKey?.ToString(),
|
|
|
|
|
Claim = args.Claim
|
|
|
|
|
});
|
|
|
|
|
/*
|
|
|
|
|
* We'll use the transition state between calls for better performance.
|
|
|
|
|
*/
|
|
|
|
|
var state = new Dictionary<string, object>();
|
|
|
|
|
/*
|
|
|
|
|
* The first state is to find out if we need to perform the actual authorization.
|
|
|
|
|
* If at least one of the providers returns success from this stage, the authorization
|
|
|
|
|
* will immediately return. For example, if the user has Full Control role there is no
|
|
|
|
|
* need to perform full authorization since the user has access to any resource.
|
|
|
|
|
*/
|
|
|
|
|
foreach (var i in await Middleware.Value)
|
|
|
|
|
{
|
|
|
|
|
if (await i.PreAuthorize(args, state) == AuthorizationProviderResult.Success)
|
|
|
|
|
return AuthorizationResult.OK();
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* If there are not permissions found we'll look into the arguments configuration. Optimistic scenarios
|
|
|
|
|
* will succeed authorization, pessimistic not.
|
|
|
|
|
*/
|
|
|
|
|
if (permissions is null || !permissions.Any())
|
|
|
|
|
{
|
|
|
|
|
return args.Schema.EmptyPolicy switch
|
|
|
|
|
{
|
|
|
|
|
EmptyPolicyBehavior.Deny => AuthorizationResult.Fail(AuthorizationResultReason.Empty),
|
|
|
|
|
EmptyPolicyBehavior.Alow => AuthorizationResult.OK(),
|
|
|
|
|
_ => throw new NotSupportedException(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* We have permissions, let's do the authorization
|
|
|
|
|
*/
|
|
|
|
|
var denyFound = false;
|
|
|
|
|
var allowFound = false;
|
|
|
|
|
/*
|
|
|
|
|
* We must authorize permission by permission.
|
|
|
|
|
*/
|
|
|
|
|
foreach (var permission in permissions)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* We'll ask every provider to tell us what he thinks about each
|
|
|
|
|
* permission and then, based on its result, we're gonna decide
|
|
|
|
|
* what to do.
|
|
|
|
|
*/
|
|
|
|
|
foreach (var provider in await Middleware.Value)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* Perform the authorization by the provider.
|
|
|
|
|
*/
|
|
|
|
|
var result = await provider.Authorize(permission, args, state);
|
|
|
|
|
/*
|
|
|
|
|
* Mark its result for the decision.
|
|
|
|
|
*/
|
|
|
|
|
switch (result)
|
|
|
|
|
{
|
|
|
|
|
case AuthorizationProviderResult.Success:
|
|
|
|
|
allowFound = true;
|
|
|
|
|
break;
|
|
|
|
|
case AuthorizationProviderResult.Fail:
|
|
|
|
|
denyFound = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* The first scenarios is that authorization succedded. If the strategy is also optimistic
|
|
|
|
|
* it means we were looking for the first successfull authorization. If found, the authorization
|
|
|
|
|
* completes successfully.
|
|
|
|
|
*
|
|
|
|
|
* The second scenarios is oposite. The authorization failed and the strategy is pessimistic which means
|
|
|
|
|
* we were looking for the first unsuccessful authorization. If found, the authorization completes unsuccessfully.
|
|
|
|
|
*/
|
|
|
|
|
if (result == AuthorizationProviderResult.Success && args.Schema.Strategy == AuthorizationStrategy.Optimistic)
|
|
|
|
|
return AuthorizationResult.OK();
|
|
|
|
|
else if (result == AuthorizationProviderResult.Fail && args.Schema.Strategy == AuthorizationStrategy.Pessimistic)
|
|
|
|
|
return AuthorizationResult.Fail(AuthorizationResultReason.Other);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* No authorization check directly decided the result so the last stage is to determine the overall result.
|
|
|
|
|
*/
|
|
|
|
|
switch (args.Schema.Strategy)
|
|
|
|
|
{
|
|
|
|
|
case AuthorizationStrategy.Pessimistic:
|
|
|
|
|
/*
|
|
|
|
|
* Pessimistic scenario where not unsuccessful authorization occured we need at least one allow. If there are
|
|
|
|
|
* no allow permissions the authorization fail.
|
|
|
|
|
*/
|
|
|
|
|
if (allowFound)
|
|
|
|
|
return AuthorizationResult.OK();
|
|
|
|
|
else
|
|
|
|
|
return AuthorizationResult.Fail(AuthorizationResultReason.NoAllowFound);
|
|
|
|
|
case AuthorizationStrategy.Optimistic:
|
|
|
|
|
/*
|
|
|
|
|
* Optimistic scenario, where no successful authorizations occured we are looking for the explicit
|
|
|
|
|
* deny authorizations. If found, the authorization fails.
|
|
|
|
|
*/
|
|
|
|
|
if (denyFound)
|
|
|
|
|
return AuthorizationResult.Fail(AuthorizationResultReason.DenyFound);
|
|
|
|
|
else
|
|
|
|
|
return AuthorizationResult.OK();
|
|
|
|
|
default:
|
|
|
|
|
throw new NotSupportedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|