Implementing IAuthorizationService

#7
This commit is contained in:
Sebastien Ros 2014-04-24 16:16:15 -07:00
parent 9726a26056
commit f5200a1d55
10 changed files with 599 additions and 1 deletions

View File

@ -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

View File

@ -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
{
/// <summary>
/// Contains authorization information used by <see cref="IAuthorizationPolicy"/>.
/// </summary>
public class AuthorizationPolicyContext
{
public AuthorizationPolicyContext(IEnumerable<Claim> claims, ClaimsPrincipal user, object resource )
{
Claims = (claims ?? Enumerable.Empty<Claim>()).ToList();
User = user;
Resource = resource;
}
/// <summary>
/// The list of claims the <see cref="IAuthorizationService"/> is checking.
/// </summary>
public IList<Claim> Claims { get; private set; }
/// <summary>
/// The user to check the claims against.
/// </summary>
public ClaimsPrincipal User { get; private set; }
/// <summary>
/// An optional resource associated to the check.
/// </summary>
public object Resource { get; private set; }
/// <summary>
/// Gets or set whether the permission will be granted to the user.
/// </summary>
public bool Authorized { get; set; }
/// <summary>
/// When set to <value>true</value>, the authorization check will be processed again.
/// </summary>
public bool Retry { get; set; }
}
}

View File

@ -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<IAuthorizationPolicy> _policies;
public int MaxRetries = 99;
public DefaultAuthorizationService(IEnumerable<IAuthorizationPolicy> policies)
{
_policies = policies.OrderBy(x => x.Order).ToArray();
}
public async Task<bool> AuthorizeAsync(IEnumerable<Claim> claims, ClaimsPrincipal user)
{
return await AuthorizeAsync(claims, user, null);
}
public bool Authorize(IEnumerable<Claim> claims, ClaimsPrincipal user)
{
return AuthorizeAsync(claims, user, null).Result;
}
public async Task<bool> AuthorizeAsync(IEnumerable<Claim> 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<Claim> claims, ClaimsPrincipal user, object resource)
{
return AuthorizeAsync(claims, user, resource).Result;
}
}
}

View File

@ -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);
}
}

View File

@ -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
{
/// <summary>
/// Checks claims based permissions for a user.
/// </summary>
public interface IAuthorizationService
{
/// <summary>
/// Checks if a user has specific claims.
/// </summary>
/// <param name="claims">The claims to check against a specific user.</param>
/// <param name="user">The user to check claims against.</param>
/// <returns><value>true</value> when the user fulfills one of the claims, <value>false</value> otherwise.</returns>
Task<bool> AuthorizeAsync(IEnumerable<Claim> claims, ClaimsPrincipal user);
/// <summary>
/// Checks if a user has specific claims.
/// </summary>
/// <param name="claims">The claims to check against a specific user.</param>
/// <param name="user">The user to check claims against.</param>
/// <returns><value>true</value> when the user fulfills one of the claims, <value>false</value> otherwise.</returns>
bool Authorize(IEnumerable<Claim> claims, ClaimsPrincipal user);
/// <summary>
/// Checks if a user has specific claims for a specific context obj.
/// </summary>
/// <param name="claims">The claims to check against a specific user.</param>
/// <param name="user">The user to check claims against.</param>
/// <param name="resource">The resource the claims should be check with.</param>
/// <returns><value>true</value> when the user fulfills one of the claims, <value>false</value> otherwise.</returns>
Task<bool> AuthorizeAsync(IEnumerable<Claim> claims, ClaimsPrincipal user, object resource);
/// <summary>
/// Checks if a user has specific claims for a specific context obj.
/// </summary>
/// <param name="claims">The claims to check against a specific user.</param>
/// <param name="user">The user to check claims against.</param>
/// <param name="resource">The resource the claims should be check with.</param>
/// <returns><value>true</value> when the user fulfills one of the claims, <value>false</value> otherwise.</returns>
bool Authorize(IEnumerable<Claim> claims, ClaimsPrincipal user, object resource);
}
}

View File

@ -25,6 +25,10 @@
<Compile Include="AuthenticationMode.cs" />
<Compile Include="AuthenticationOptions.cs" />
<Compile Include="AuthenticationTicket.cs" />
<Compile Include="Authorization\AuthorizationPolicyContext.cs" />
<Compile Include="Authorization\DefaultAuthorizationService.cs" />
<Compile Include="Authorization\IAuthorizationPolicy.cs" />
<Compile Include="Authorization\IAuthorizationService.cs" />
<Compile Include="CertificateSubjectKeyIdentifierValidator.cs" />
<Compile Include="CertificateSubjectPublicKeyInfoValidator.cs" />
<Compile Include="CertificateThumbprintValidator.cs" />

View File

@ -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<IAuthorizationPolicy>());
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<IAuthorizationPolicy>());
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<IAuthorizationPolicy>());
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<IAuthorizationPolicy>());
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<IAuthorizationPolicy>());
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<IAuthorizationPolicy>());
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<IAuthorizationPolicy>());
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<Claim> 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<AggregateException>(() => 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<Claim>(), user);
// Assert
Assert.True(allowed);
}
}
}

View File

@ -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<AuthorizationPolicyContext> ApplyingAsyncAction { get; set;}
public Action<AuthorizationPolicyContext> ApplyAsyncAction { get; set;}
public Action<AuthorizationPolicyContext> AppliedAsyncAction { get; set;}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\ProjectK\Microsoft.Web.ProjectK.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>8da26cd1-1302-4cfd-9270-9fa1b7c6138b</ProjectGuid>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="Project.json" />
</ItemGroup>
<ItemGroup>
<Compile Include="DefaultAuthorizationServiceTests.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<Import Project="$(VSToolsPath)\ProjectK\Microsoft.Web.ProjectK.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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": ""
}
}
}
}