From f5200a1d55e625fbe122c30664c5021cde8d645c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 24 Apr 2014 16:16:15 -0700 Subject: [PATCH] Implementing IAuthorizationService #7 --- Security.sln | 17 +- .../AuthorizationPolicyContext.cs | 46 +++ .../DefaultAuthorizationService.cs | 92 ++++++ .../Authorization/IAuthorizationPolicy.cs | 13 + .../Authorization/IAuthorizationService.cs | 49 ++++ .../Microsoft.AspNet.Security.kproj | 4 + .../DefaultAuthorizationServiceTests.cs | 276 ++++++++++++++++++ .../FakePolicy.cs | 49 ++++ .../Microsoft.AspNet.Security.kproj | 29 ++ .../project.json | 25 ++ 10 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs create mode 100644 src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs create mode 100644 src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs create mode 100644 src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs create mode 100644 test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs create mode 100644 test/Microsoft.AspNet.Security.Test/FakePolicy.cs create mode 100644 test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj create mode 100644 test/Microsoft.AspNet.Security.Test/project.json diff --git a/Security.sln b/Security.sln index 8cd81e5086..752ce9ab0c 100644 --- a/Security.sln +++ b/Security.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.30401.0 +VisualStudioVersion = 12.0.30411.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}" EndProject @@ -13,6 +13,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.C EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSample", "samples\CookieSample\CookieSample.kproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7BF11F3A-60B6-4796-B504-579C67FFBA34}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security", "test\Microsoft.AspNet.Security.Test\Microsoft.AspNet.Security.kproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +57,16 @@ Global {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.Build.0 = Release|x86 {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.ActiveCfg = Release|x86 {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.Build.0 = Release|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.ActiveCfg = Debug|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.Build.0 = Debug|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.ActiveCfg = Release|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|x86 + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -61,5 +75,6 @@ Global {0F174C63-1898-4024-9A3C-3FDF5CAE5C68} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {15F1211B-B695-4A1C-B730-1AC58FC91090} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {558C2C2A-AED8-49DE-BB60-D5F8AE06C714} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs b/src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs new file mode 100644 index 0000000000..ab65513ab5 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Linq; + +namespace Microsoft.AspNet.Security.Authorization +{ + /// + /// Contains authorization information used by . + /// + public class AuthorizationPolicyContext + { + public AuthorizationPolicyContext(IEnumerable claims, ClaimsPrincipal user, object resource ) + { + Claims = (claims ?? Enumerable.Empty()).ToList(); + User = user; + Resource = resource; + } + + /// + /// The list of claims the is checking. + /// + public IList Claims { get; private set; } + + /// + /// The user to check the claims against. + /// + public ClaimsPrincipal User { get; private set; } + + /// + /// An optional resource associated to the check. + /// + public object Resource { get; private set; } + + /// + /// Gets or set whether the permission will be granted to the user. + /// + public bool Authorized { get; set; } + + /// + /// When set to true, the authorization check will be processed again. + /// + public bool Retry { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs new file mode 100644 index 0000000000..7faf3430e3 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security.Authorization +{ + public class DefaultAuthorizationService : IAuthorizationService + { + private readonly IList _policies; + public int MaxRetries = 99; + + public DefaultAuthorizationService(IEnumerable policies) + { + _policies = policies.OrderBy(x => x.Order).ToArray(); + } + + public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user) + { + return await AuthorizeAsync(claims, user, null); + } + + public bool Authorize(IEnumerable claims, ClaimsPrincipal user) + { + return AuthorizeAsync(claims, user, null).Result; + } + + public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource) + { + var context = new AuthorizationPolicyContext(claims, user, resource); + + foreach (var policy in _policies) + { + await policy.ApplyingAsync(context); + } + + // we only apply the policies for a limited number of times to prevent + // infinite loops + + int retries; + for (retries = 0; retries < MaxRetries; retries++) + { + // we don't need to check for owned claims if the permission is already granted + if (!context.Authorized) + { + if (context.User != null) + { + if (context.Claims.Any(claim => user.HasClaim(claim.Type, claim.Value))) + { + context.Authorized = true; + } + } + } + + // reset the retry flag + context.Retry = false; + + // give a chance for policies to change claims or the grant + foreach (var policy in _policies) + { + await policy.ApplyAsync(context); + } + + // if no policies have changed the context, stop checking + if (!context.Retry) + { + break; + } + } + + if (retries == MaxRetries) + { + throw new InvalidOperationException("Too many authorization retries."); + } + + foreach (var policy in _policies) + { + await policy.AppliedAsync(context); + } + + return context.Authorized; + } + + public bool Authorize(IEnumerable claims, ClaimsPrincipal user, object resource) + { + return AuthorizeAsync(claims, user, resource).Result; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs new file mode 100644 index 0000000000..8100ae32f1 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Threading.Tasks; +namespace Microsoft.AspNet.Security.Authorization +{ + public interface IAuthorizationPolicy + { + int Order { get; set; } + Task ApplyingAsync(AuthorizationPolicyContext context); + Task ApplyAsync(AuthorizationPolicyContext context); + Task AppliedAsync(AuthorizationPolicyContext context); + } +} diff --git a/src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs new file mode 100644 index 0000000000..30284e4c54 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security.Authorization +{ + /// + /// Checks claims based permissions for a user. + /// + public interface IAuthorizationService + { + /// + /// Checks if a user has specific claims. + /// + /// The claims to check against a specific user. + /// The user to check claims against. + /// true when the user fulfills one of the claims, false otherwise. + Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user); + + /// + /// Checks if a user has specific claims. + /// + /// The claims to check against a specific user. + /// The user to check claims against. + /// true when the user fulfills one of the claims, false otherwise. + bool Authorize(IEnumerable claims, ClaimsPrincipal user); + + /// + /// Checks if a user has specific claims for a specific context obj. + /// + /// The claims to check against a specific user. + /// The user to check claims against. + /// The resource the claims should be check with. + /// true when the user fulfills one of the claims, false otherwise. + Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource); + + /// + /// Checks if a user has specific claims for a specific context obj. + /// + /// The claims to check against a specific user. + /// The user to check claims against. + /// The resource the claims should be check with. + /// true when the user fulfills one of the claims, false otherwise. + bool Authorize(IEnumerable claims, ClaimsPrincipal user, object resource); + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj b/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj index 26a8109a53..37943b7510 100644 --- a/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj +++ b/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj @@ -25,6 +25,10 @@ + + + + diff --git a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs new file mode 100644 index 0000000000..8be366974b --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Security.Authorization; +using Xunit; + +namespace Microsoft.AspNet.Security.Test +{ + public class DefaultAuthorizationServiceTests + { + [Fact] + public void Check_ShouldAllowIfClaimIsPresent() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var user = new ClaimsPrincipal( + new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic") + ); + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.True(allowed); + } + + [Fact] + public void Check_ShouldAllowIfClaimIsAmongValues() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + new Claim("Permission", "CanViewAnything") + }, + "Basic") + ); + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.True(allowed); + } + + [Fact] + public void Check_ShouldNotAllowIfClaimTypeIsNotPresent() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.False(allowed); + } + + [Fact] + public void Check_ShouldNotAllowIfClaimValueIsNotPresent() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), + }, + "Basic") + ); + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.False(allowed); + } + + [Fact] + public void Check_ShouldNotAllowIfNoClaims() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[0], + "Basic") + ); + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.False(allowed); + } + + [Fact] + public void Check_ShouldNotAllowIfUserIsNull() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + ClaimsPrincipal user = null; + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.False(allowed); + } + + [Fact] + public void Check_ShouldNotAllowIfUserIsNotAuthenticated() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), + }, + null) + ); + + // Act + var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + + // Assert + Assert.False(allowed); + } + + [Fact] + public void Check_ShouldApplyPoliciesInOrder() + { + // Arrange + string result = ""; + var policies = new IAuthorizationPolicy[] { + new FakePolicy() { + Order = 20, + ApplyingAsyncAction = (context) => { result += "20"; } + }, + new FakePolicy() { + Order = -1, + ApplyingAsyncAction = (context) => { result += "-1"; } + }, + new FakePolicy() { + Order = 30, + ApplyingAsyncAction = (context) => { result += "30"; } + }, + }; + + var authorizationService = new DefaultAuthorizationService(policies); + + // Act + var allowed = authorizationService.Authorize(null, null); + + // Assert + Assert.Equal("-12030", result); + } + + [Fact] + public void Check_ShouldInvokeApplyingApplyAppliedInOrder() + { + // Arrange + string result = ""; + var policies = new IAuthorizationPolicy[] { + new FakePolicy() { + Order = 20, + ApplyingAsyncAction = (context) => { result += "Applying20"; }, + ApplyAsyncAction = (context) => { result += "Apply20"; }, + AppliedAsyncAction = (context) => { result += "Applied20"; } + }, + new FakePolicy() { + Order = -1, + ApplyingAsyncAction = (context) => { result += "Applying-1"; }, + ApplyAsyncAction = (context) => { result += "Apply-1"; }, + AppliedAsyncAction = (context) => { result += "Applied-1"; } + }, + new FakePolicy() { + Order = 30, + ApplyingAsyncAction = (context) => { result += "Applying30"; }, + ApplyAsyncAction = (context) => { result += "Apply30"; }, + AppliedAsyncAction = (context) => { result += "Applied30"; } + }, + }; + + var authorizationService = new DefaultAuthorizationService(policies); + + // Act + var allowed = authorizationService.Authorize(null, null); + + // Assert + Assert.Equal("Applying-1Applying20Applying30Apply-1Apply20Apply30Applied-1Applied20Applied30", result); + } + + [Fact] + public void Check_ShouldConvertNullClaimsToEmptyList() + { + // Arrange + IList claims = null; + var policies = new IAuthorizationPolicy[] { + new FakePolicy() { + Order = 20, + ApplyingAsyncAction = (context) => { claims = context.Claims; } + } + }; + + var authorizationService = new DefaultAuthorizationService(policies); + + // Act + var allowed = authorizationService.Authorize(null, null); + + // Assert + Assert.NotNull(claims); + Assert.Equal(0, claims.Count); + } + + [Fact] + public void Check_ShouldThrowWhenPoliciesDontStop() + { + // Arrange + var policies = new IAuthorizationPolicy[] { + new FakePolicy() { + ApplyAsyncAction = (context) => { context.Retry = true; } + } + }; + + var authorizationService = new DefaultAuthorizationService(policies); + + // Act + // Assert + Exception ex = Assert.Throws(() => authorizationService.Authorize(null, null)); + } + + [Fact] + public void Check_ApplyCanMutateClaims() + { + + // Arrange + var user = new ClaimsPrincipal( + new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanDeleteComments") }, "Basic") + ); + + var policies = new IAuthorizationPolicy[] { + new FakePolicy() { + ApplyAsyncAction = (context) => { + // for instance, if user owns the comment + if(!context.Claims.Any(claim => claim.Type == "Permission" && claim.Value == "CanDeleteComments")) + { + context.Claims.Add(new Claim("Permission", "CanDeleteComments")); + context.Retry = true; + } + } + } + }; + + var authorizationService = new DefaultAuthorizationService(policies); + + // Act + var allowed = authorizationService.Authorize(Enumerable.Empty(), user); + + // Assert + Assert.True(allowed); + } + } +} diff --git a/test/Microsoft.AspNet.Security.Test/FakePolicy.cs b/test/Microsoft.AspNet.Security.Test/FakePolicy.cs new file mode 100644 index 0000000000..a3bdf8c1f9 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/FakePolicy.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Security.Authorization; + +namespace Microsoft.AspNet.Security.Test +{ + public class FakePolicy : IAuthorizationPolicy + { + + public int Order { get; set; } + + public Task ApplyingAsync(AuthorizationPolicyContext context) + { + if (ApplyingAsyncAction != null) + { + ApplyingAsyncAction(context); + } + + return Task.FromResult(0); + } + + public Task ApplyAsync(AuthorizationPolicyContext context) + { + if (ApplyAsyncAction != null) + { + ApplyAsyncAction(context); + } + + return Task.FromResult(0); + + } + + public Task AppliedAsync(AuthorizationPolicyContext context) + { + if (AppliedAsyncAction != null) + { + AppliedAsyncAction(context); + } + + return Task.FromResult(0); + } + + public Action ApplyingAsyncAction { get; set;} + + public Action ApplyAsyncAction { get; set;} + + public Action AppliedAsyncAction { get; set;} + } +} diff --git a/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj b/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj new file mode 100644 index 0000000000..8f60c5ab82 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj @@ -0,0 +1,29 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 8da26cd1-1302-4cfd-9270-9fa1b7c6138b + Library + + + + + + + 2.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json new file mode 100644 index 0000000000..cfdc5ed7f9 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/project.json @@ -0,0 +1,25 @@ +{ + "version" : "0.1-alpha-*", + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.Security" : "0.1-alpha-*", + "Moq": "4.2.1312.1622", + "Xunit.KRunner": "0.1-alpha-*", + "xunit.abstractions": "2.0.0-aspnet-*", + "xunit.assert": "2.0.0-aspnet-*", + "xunit.core": "2.0.0-aspnet-*", + "xunit.execution": "2.0.0-aspnet-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "configurations": { + "net45": { + "dependencies": { + "System.Runtime": "" + } + } + } +}