using Connected.Interop; using Connected.Middleware; using Connected.Security.Authorization; using Connected.ServiceModel; using Connected.Services.Annotations; using Connected.Threading; using System.Collections.Immutable; using System.Reflection; namespace Connected.Services.Authorization; internal sealed class ServiceAuthorizationContext : IAuthorizationContext { public ServiceAuthorizationContext(IAuthorizationService authorization, IMiddlewareService middleware) { Authorization = authorization; Middleware = new AsyncLazy>(middleware.Query()); } private IAuthorizationService Authorization { get; } private AsyncLazy> Middleware { get; } public AuthorizationContextState State { get; private set; } = AuthorizationContextState.Pending; public async Task Authorize(AuthorizationArgs args) { /* * Authorization is performed only if the context does not have a grant permission * for the execution. That means only the first authorization request is actually * performed. Once the caller has been granted access no authorization takes place * afterwards. */ if (State == AuthorizationContextState.Granted) return AuthorizationResult.OK(); var result = await Authorization.Authorize(args); /* * Authorization completed. Set correct state which means no * authorization is needed for any subsequent calls. */ if (result.Success) State = AuthorizationContextState.Granted; else State = AuthorizationContextState.Revoked; return result; } public async Task Authorize(ICallerContext context, TArgs args) where TArgs : IDto { /* * We allow only one authorizaton process in the same context at the same time. * We would cause a potential stack overflow otherwise. */ if (State == AuthorizationContextState.Authorizing) return; if (State == AuthorizationContextState.Granted) return; State = AuthorizationContextState.Authorizing; try { if (context.Sender is null) return; if (ResolveAttributes(context, args) is not List attributes || !attributes.Any()) return; var staging = attributes.Where(f => f.Stage == AuthorizationStage.Init); if (!staging.Any()) return; Exception? firstFail = null; bool onePassed = false; foreach (var attribute in staging) { try { if (attribute.Behavior == AuthorizationPolicyBehavior.Optional && onePassed) continue; var claims = attribute.Claims.ToImmutableArray(); foreach (var middleware in await Middleware.Value) claims = await middleware.ResolveClaims(claims); var middlewareArgs = new ServiceAuthorizationMiddlewareArgs(context, claims, args); foreach (var middleware in await Middleware.Value) await middleware.Authorize(middlewareArgs); onePassed = true; } catch (Exception ex) { if (attribute.Behavior == AuthorizationPolicyBehavior.Mandatory) throw; firstFail = ex; } } if (!onePassed && firstFail is not null) throw firstFail; State = AuthorizationContextState.Granted; } catch { State = AuthorizationContextState.Revoked; throw; } } /// /// Results are always authorized, event if is . /// /// /// /// /// /// /// public async Task Authorize(ICallerContext context, TArgs args, TComponent component) where TArgs : IDto { if (context.Sender is null) return component; if (ResolveAttributes(context, args) is not List attributes || !attributes.Any()) return component; var staging = attributes.Where(f => f.Stage == AuthorizationStage.Result); if (!staging.Any()) return component; Exception? firstFail = null; bool onePassed = false; var result = component; foreach (var attribute in staging) { try { if (attribute.Behavior == AuthorizationPolicyBehavior.Optional && onePassed) continue; var claims = attribute.Claims.ToImmutableArray(); foreach (var middleware in await Middleware.Value) claims = await middleware.ResolveClaims(claims); var middlewareArgs = new ServiceAuthorizationMiddlewareArgs(context, claims, args); foreach (var middleware in await Middleware.Value) result = await middleware.Authorize(middlewareArgs, result); onePassed = true; } catch (Exception ex) { if (attribute.Behavior == AuthorizationPolicyBehavior.Mandatory) throw; firstFail = ex; } } if (!onePassed && firstFail is not null) throw firstFail; return result; } public void Revoke() { if (State == AuthorizationContextState.Granted) State = AuthorizationContextState.Revoked; } private static List? ResolveAttributes(ICallerContext context, TArgs args) where TArgs : IDto { if (context.Sender is null || string.IsNullOrEmpty(context.Method)) return default; if (context.Sender.GetType().ResolveMethod(context.Method, null, new Type[1] { args.GetType() }) is not MethodInfo method) throw new InvalidOperationException($"{SR.ErrMethodResolve} ({context.Sender.GetType().Name}.{context.Method})"); var attributes = method.GetCustomAttributes(typeof(ServiceAuthorizationAttribute), false); if (attributes is null || !attributes.Any()) return null; var result = new List(); foreach (var attribute in attributes) result.Add((ServiceAuthorizationAttribute)attribute); result.Sort((left, right) => { if (left.Priority < right.Priority) return -1; else if (left.Priority == right.Priority) return 0; else return 1; }); return result; } }