diff --git a/Security.sln b/Security.sln index 2ed873270..3f8e3004e 100644 --- a/Security.sln +++ b/Security.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}" EndProject @@ -57,6 +57,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Owin.Security.Int EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIdConnect.AzureAdSample", "samples\OpenIdConnect.AzureAdSample\OpenIdConnect.AzureAdSample.xproj", "{3A7AD414-EBDE-4F92-B307-4E8F19B6117E}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AuthorizationSample", "samples\AuthorizationSample\AuthorizationSample.xproj", "{CE608B3F-5533-41B8-BC54-AE822AAAEDEB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -325,6 +327,18 @@ Global {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x86.ActiveCfg = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x86.Build.0 = Release|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Debug|x86.Build.0 = Debug|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Release|Any CPU.Build.0 = Release|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Release|x86.ActiveCfg = Release|Any CPU + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -352,5 +366,6 @@ Global {A7922DD8-09F1-43E4-938B-CC523EA08898} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} {3A7AD414-EBDE-4F92-B307-4E8F19B6117E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {CE608B3F-5533-41B8-BC54-AE822AAAEDEB} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} EndGlobalSection EndGlobal diff --git a/samples/AuthorizationSample/AuthorizationSample.xproj b/samples/AuthorizationSample/AuthorizationSample.xproj new file mode 100644 index 000000000..27ee06b2d --- /dev/null +++ b/samples/AuthorizationSample/AuthorizationSample.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ce608b3f-5533-41b8-bc54-ae822aaaedeb + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\ + + + 2.0 + 22569 + + + \ No newline at end of file diff --git a/samples/AuthorizationSample/Customer.cs b/samples/AuthorizationSample/Customer.cs new file mode 100644 index 000000000..2e0b50ee9 --- /dev/null +++ b/samples/AuthorizationSample/Customer.cs @@ -0,0 +1,10 @@ +namespace AuthorizationSample +{ + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public string Region { get; set; } + public bool Fortune500 { get; set; } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/CustomerOperationAuthorizationHandler.cs b/samples/AuthorizationSample/CustomerOperationAuthorizationHandler.cs new file mode 100644 index 000000000..9072147a1 --- /dev/null +++ b/samples/AuthorizationSample/CustomerOperationAuthorizationHandler.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace AuthorizationSample +{ + public class CustomerOperationAuthorizationHandler : + AuthorizationHandler + { + private readonly IPermissionService _permissions; + + public CustomerOperationAuthorizationHandler(IPermissionService permissions) + { + _permissions = permissions; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + OperationAuthorizationRequirement requirement, + Customer resource) + { + if (!context.User.HasClaim("department", "sales")) + { + return; + } + + if (!context.User.HasClaim("region", resource.Region)) + { + return; + } + + if (resource.Fortune500) + { + if (!context.User.HasClaim("status", "senior")) + { + return; + } + } + + var discountOperationAuthorizationRequirement = requirement as DiscountOperationAuthorizationRequirement; + if (discountOperationAuthorizationRequirement != null) + { + var salesRep = context.User.FindFirst("id").Value; + if (!await _permissions.IsDiscountAllowedAsync( + salesRep, + resource.Id, + discountOperationAuthorizationRequirement.Amount)) + { + return; + } + } + + context.Succeed(requirement); + } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/CustomerOperations.cs b/samples/AuthorizationSample/CustomerOperations.cs new file mode 100644 index 000000000..ff06c104e --- /dev/null +++ b/samples/AuthorizationSample/CustomerOperations.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace AuthorizationSample +{ + public static class CustomerOperations + { + public static OperationAuthorizationRequirement Manage = new OperationAuthorizationRequirement {Name = "Manage"}; + + public static OperationAuthorizationRequirement GiveDiscount(int amount) + { + return new DiscountOperationAuthorizationRequirement + { + Name = "GiveDiscount" + amount, + Amount = amount + }; + } + } + + public class DiscountOperationAuthorizationRequirement : OperationAuthorizationRequirement + { + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/PermissionService.cs b/samples/AuthorizationSample/PermissionService.cs new file mode 100644 index 000000000..528eb25bf --- /dev/null +++ b/samples/AuthorizationSample/PermissionService.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace AuthorizationSample +{ + public interface IPermissionService + { + Task IsDiscountAllowedAsync(string id, int customerId, int amount); + } + + public class PermissionService : IPermissionService + { + public async Task IsDiscountAllowedAsync(string id, int customerId, int amount) + { + await Task.Delay(100); + return amount < 10; + } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/Program.cs b/samples/AuthorizationSample/Program.cs new file mode 100644 index 000000000..474a67b81 --- /dev/null +++ b/samples/AuthorizationSample/Program.cs @@ -0,0 +1,20 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; + +namespace AuthorizationSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/Properties/launchSettings.json b/samples/AuthorizationSample/Properties/launchSettings.json new file mode 100644 index 000000000..afe862f4f --- /dev/null +++ b/samples/AuthorizationSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1788/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "CookieSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:12345", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:12345" + } + } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/Startup.cs b/samples/AuthorizationSample/Startup.cs new file mode 100644 index 000000000..c2dbe6ac9 --- /dev/null +++ b/samples/AuthorizationSample/Startup.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AuthorizationSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(); + + services.AddAuthorization(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + { + loggerfactory.AddConsole(LogLevel.Information); + + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AutomaticAuthenticate = true + }); + + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + var authorizationService = context.RequestServices.GetService(); + + var users = new[] + { + new[] + { + new Claim("id", "1"), + new Claim("name", "Alice"), + new Claim("email", "alice@smith.com"), + new Claim("status", "intern"), + new Claim("department", "sales"), + new Claim("region", "north") + }, + new[] + { + new Claim("id", "2"), + new Claim("name", "Bob"), + new Claim("email", "bob@smith.com"), + new Claim("status", "senior"), + new Claim("department", "sales"), + new Claim("region", "north") + }, + new[] + { + new Claim("id", "3"), + new Claim("name", "John"), + new Claim("email", "john@doe.com"), + new Claim("role", "supervisor"), + new Claim("department", "finance") + } + }.Select( + c => + new ClaimsPrincipal(new ClaimsIdentity(c, CookieAuthenticationDefaults.AuthenticationScheme, + "name", "role"))); + + var operations = new[] + {CustomerOperations.Manage, CustomerOperations.GiveDiscount(5), CustomerOperations.GiveDiscount(15)}; + + var customers = new[] + { + new Customer + { + Name = "Acme Corp", + Region = "north", + Fortune500 = false + }, + new Customer + { + Name = "Bcme Corp", + Region = "south", + Fortune500 = false + }, + new Customer + { + Name = "Jcme Corp", + Region = "north", + Fortune500 = true + } + }; + + foreach (var user in users) + { + foreach (var operation in operations) + { + foreach (var customer in customers) + { + var isAuthorized = await authorizationService.AuthorizeAsync(user, customer, operation); + await context.Response.WriteAsync( + $"user {user.FindFirst("name").Value,-15} isAuthorized: {isAuthorized,-15} for operation: {operation.Name,-15} with customer: {customer.Name,-15} {Environment.NewLine}"); + } + } + } + }); + } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/SupervisorOperationHandler.cs b/samples/AuthorizationSample/SupervisorOperationHandler.cs new file mode 100644 index 000000000..fbdfad071 --- /dev/null +++ b/samples/AuthorizationSample/SupervisorOperationHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace AuthorizationSample +{ + public class SupervisorOperationHandler : AuthorizationHandler + { + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + OperationAuthorizationRequirement requirement) + { + if (!context.User.IsInRole("supervisor")) + { + return; + } + + context.Succeed(requirement); + } + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/project.json b/samples/AuthorizationSample/project.json new file mode 100644 index 000000000..7e0305973 --- /dev/null +++ b/samples/AuthorizationSample/project.json @@ -0,0 +1,37 @@ +{ + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1-*", + "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNetCore.Authorization": "1.0.0-*", + "Microsoft.AspNetCore.DataProtection": "1.0.0-*", + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-*", + "Microsoft.Extensions.Logging.Console": "1.0.0-*" + }, + "buildOptions": { + "emitEntryPoint": true + }, + "frameworks": { + "net451": {}, + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + } + }, + "publishOptions": { + "include": [ + "web.config" + ] + }, + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-*" + }, + "scripts": { + "postpublish": "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" + } +} \ No newline at end of file diff --git a/samples/AuthorizationSample/web.config b/samples/AuthorizationSample/web.config new file mode 100644 index 000000000..36be478b0 --- /dev/null +++ b/samples/AuthorizationSample/web.config @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs index 5dc57c278..166cd15e2 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; namespace Microsoft.AspNetCore.Authorization @@ -13,10 +13,6 @@ namespace Microsoft.AspNetCore.Authorization /// public class AuthorizationHandlerContext { - private HashSet _pendingRequirements; - private bool _failCalled; - private bool _succeedCalled; - /// /// Creates a new instance of . /// @@ -36,7 +32,8 @@ public AuthorizationHandlerContext( Requirements = requirements; User = user; Resource = resource; - _pendingRequirements = new HashSet(requirements); + FailedRequirements = new ConcurrentQueue(); + SucceededRequirements = new ConcurrentQueue(); } /// @@ -54,45 +51,23 @@ public AuthorizationHandlerContext( /// public object Resource { get; } - /// - /// Gets the requirements that have not yet been marked as succeeded. - /// - public IEnumerable PendingRequirements { get { return _pendingRequirements; } } + public ConcurrentQueue FailedRequirements { get; } - /// - /// Flag indicating whether the current authorization processing has failed. - /// - public bool HasFailed { get { return _failCalled; } } + public ConcurrentQueue SucceededRequirements { get; } - /// - /// Flag indicating whether the current authorization processing has succeeded. - /// - public bool HasSucceeded + public void Fail(IAuthorizationRequirement requirement = null) { - get - { - return !_failCalled && _succeedCalled && !PendingRequirements.Any(); - } + FailedRequirements.Enqueue(requirement); } - /// - /// Called to indicate will - /// never return true, even if all requirements are met. - /// - public void Fail() - { - _failCalled = true; - } - - /// - /// Called to mark the specified as being - /// successfully evaluated. - /// - /// The requirement whose evaluation has succeeded. public void Succeed(IAuthorizationRequirement requirement) { - _succeedCalled = true; - _pendingRequirements.Remove(requirement); + if (requirement == null) + { + throw new ArgumentNullException(nameof(requirement)); + } + + SucceededRequirements.Enqueue(requirement); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs index c92be6232..f56ea5c19 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ public static IServiceCollection AddAuthorization(this IServiceCollection servic services.TryAdd(ServiceDescriptor.Transient()); services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); services.TryAddEnumerable(ServiceDescriptor.Transient()); return services; } diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs new file mode 100644 index 000000000..f5d6f1831 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; + +namespace Microsoft.AspNetCore.Authorization +{ + public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator + { + public bool HasFailed(AuthorizationHandlerContext context) + { + return context.FailedRequirements.Any(); + } + + public bool HasSucceeded(AuthorizationHandlerContext context) + { + return !context.FailedRequirements.Any() && context.SucceededRequirements.Any() && !context.Requirements.Except(context.FailedRequirements).Except(context.SucceededRequirements).Any(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs index 6665204ee..9c41cbb3e 100644 --- a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs @@ -19,14 +19,16 @@ public class DefaultAuthorizationService : IAuthorizationService private readonly IAuthorizationPolicyProvider _policyProvider; private readonly IList _handlers; private readonly ILogger _logger; + private readonly IAuthorizationEvaluator _authorizationEvaluator; /// /// Creates a new instance of . /// /// The used to provide policies. /// The handlers used to fulfill s. - /// The logger used to log messages, warnings and errors. - public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IEnumerable handlers, ILogger logger) + /// The logger used to log messages, warnings and errors. + /// + public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IEnumerable handlers, ILogger logger, IAuthorizationEvaluator authorizationEvaluator) { if (policyProvider == null) { @@ -40,10 +42,15 @@ public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, { throw new ArgumentNullException(nameof(logger)); } + if (authorizationEvaluator == null) + { + throw new ArgumentNullException(nameof(authorizationEvaluator)); + } _handlers = handlers.ToArray(); _policyProvider = policyProvider; _logger = logger; + _authorizationEvaluator = authorizationEvaluator; } /// @@ -69,7 +76,7 @@ public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, IE await handler.HandleAsync(authContext); } - if (authContext.HasSucceeded) + if (_authorizationEvaluator.HasSucceeded(authContext)) { _logger.UserAuthorizationSucceeded(GetUserNameForLogging(user)); return true; diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs new file mode 100644 index 000000000..ad12304a7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + public interface IAuthorizationEvaluator + { + bool HasFailed(AuthorizationHandlerContext context); + + bool HasSucceeded(AuthorizationHandlerContext context); + } +} \ No newline at end of file