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>(middleware.Query()); Permissions = permissions; } private IPermissionService Permissions { get; } private AsyncLazy> Middleware { get; } public async Task 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(); /* * 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(); } } }