diff --git a/samples/MvcSample.Web/Filters/BlockAnonymous.cs b/samples/MvcSample.Web/Filters/BlockAnonymous.cs index f05cfe9f8c..5e533613d3 100644 --- a/samples/MvcSample.Web/Filters/BlockAnonymous.cs +++ b/samples/MvcSample.Web/Filters/BlockAnonymous.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; namespace MvcSample.Web.Filters { @@ -6,9 +6,18 @@ namespace MvcSample.Web.Filters { public override void OnAuthorization(AuthorizationContext context) { - if (!HasAllowAnonymous(context)) + if (!HasAllowAnonymous(context)) { - context.Result = new HttpStatusCodeResult(401); + var user = content.HttpContext.User; + var userIsAnonymous = + user == null || + user.Identity == null || + !user.Identity.IsAuthenticated; + + if(userIsAnonymous) + { + base.Fail(context); + } } } } diff --git a/samples/MvcSample.Web/Filters/FakeUserAttribute.cs b/samples/MvcSample.Web/Filters/FakeUserAttribute.cs new file mode 100644 index 0000000000..71154ac74d --- /dev/null +++ b/samples/MvcSample.Web/Filters/FakeUserAttribute.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using System.Security.Claims; + +namespace MvcSample.Web +{ + public class FakeUserAttribute : AuthorizationFilterAttribute + { + public override void OnAuthorization(AuthorizationContext context) + { + context.HttpContext.User = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + new Claim(ClaimTypes.Role, "Administrator"), + new Claim(ClaimTypes.NameIdentifier, "John")}, + "Basic")); + } + } +} diff --git a/samples/MvcSample.Web/FiltersController.cs b/samples/MvcSample.Web/FiltersController.cs index 9700648001..ddb654b7dd 100644 --- a/samples/MvcSample.Web/FiltersController.cs +++ b/samples/MvcSample.Web/FiltersController.cs @@ -38,6 +38,19 @@ namespace MvcSample.Web return Index(age, userName); } + [Authorize("Permission", "CanViewPage")] + public IActionResult NotGrantedClaim(int age, string userName) + { + return Index(age, userName); + } + + [FakeUser] + [Authorize("Permission", "CanViewPage", "CanViewAnything")] + public IActionResult AllGranted(int age, string userName) + { + return Index(age, userName); + } + [ErrorMessages, AllowAnonymous] public IActionResult Crash(string message) { diff --git a/samples/MvcSample.Web/MvcSample.Web.kproj b/samples/MvcSample.Web/MvcSample.Web.kproj index ec493b0ca7..018b593fad 100644 --- a/samples/MvcSample.Web/MvcSample.Web.kproj +++ b/samples/MvcSample.Web/MvcSample.Web.kproj @@ -44,6 +44,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs index 22a41a7f55..522c3d3cb1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs @@ -24,5 +24,10 @@ namespace Microsoft.AspNet.Mvc { return context.Filters.Any(item => item is IAllowAnonymous); } + + protected virtual void Fail([NotNull] AuthorizationContext context) + { + context.Result = new HttpStatusCodeResult(401); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizeAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizeAttribute.cs new file mode 100644 index 0000000000..3d15fad9a8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizeAttribute.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.DependencyInjection; +using Microsoft.AspNet.Security.Authorization; + +namespace Microsoft.AspNet.Mvc +{ + public class AuthorizeAttribute : AuthorizationFilterAttribute + { + protected Claim[] _claims; + + public AuthorizeAttribute() + { + _claims = new Claim[0]; + } + + public AuthorizeAttribute([NotNull]IEnumerable claims) + { + _claims = claims.ToArray(); + } + + public AuthorizeAttribute(string claimType, string claimValue) + { + _claims = new [] { new Claim(claimType, claimValue) }; + } + + public AuthorizeAttribute(string claimType, string claimValue, params string[] otherClaimValues) + : this(claimType, claimValue) + { + if (otherClaimValues.Length > 0) + { + _claims = _claims.Concat(otherClaimValues.Select(claim => new Claim(claimType, claim))).ToArray(); + } + } + + public override async Task OnAuthorizationAsync([NotNull] AuthorizationContext context) + { + var httpContext = context.HttpContext; + var user = httpContext.User; + + // when no claims are specified, we just need to ensure the user is authenticated + if (_claims.Length == 0) + { + var userIsAnonymous = + user == null || + user.Identity == null || + !user.Identity.IsAuthenticated; + + if(userIsAnonymous) + { + base.Fail(context); + } + } + else + { + var authorizationService = httpContext.RequestServices.GetService(); + + if (authorizationService == null) + { + throw new InvalidOperationException(Resources.AuthorizeAttribute_AuthorizationServiceMustBeDefined); + } + + var authorized = await authorizationService.AuthorizeAsync(_claims, user); + + if (!authorized) + { + base.Fail(context); + } + } + } + + public sealed override void OnAuthorization([NotNull] AuthorizationContext context) + { + // The async filter will be called by the filter pipeline. + throw new NotImplementedException(Resources.AuthorizeAttribute_OnAuthorizationNotImplemented); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 5c6d047837..3171a1f5b2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -61,6 +61,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 1369df543c..02a7741d8d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -682,6 +682,38 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_ViewNotFound"), p0, p1); } + /// + /// Unable to locate an implementation of IAuthorizationService. + /// + internal static string AuthorizeAttribute_AuthorizationServiceMustBeDefined + { + get { return GetString("AuthorizeAttribute_AuthorizationServiceMustBeDefined"); } + } + + /// + /// Unable to locate an implementation of IAuthorizationService. + /// + internal static string FormatAuthorizeAttribute_AuthorizationServiceMustBeDefined() + { + return GetString("AuthorizeAttribute_AuthorizationServiceMustBeDefined"); + } + + /// + /// OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead. + /// + internal static string AuthorizeAttribute_OnAuthorizationNotImplemented + { + get { return GetString("AuthorizeAttribute_OnAuthorizationNotImplemented"); } + } + + /// + /// OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead. + /// + internal static string FormatAuthorizeAttribute_OnAuthorizationNotImplemented() + { + return GetString("AuthorizeAttribute_OnAuthorizationNotImplemented"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index e2399bff48..ba83a57312 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -243,4 +243,10 @@ The view '{0}' was not found. The following locations were searched:{1}. + + Unable to locate an implementation of IAuthorizationService. + + + OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead. + diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 52d5133846..e1a8d4bb32 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -8,6 +8,7 @@ "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*", "Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.Routing": "0.1-alpha-*", + "Microsoft.AspNet.Security": "0.1-alpha-*", "Common": "", "Microsoft.AspNet.Mvc.ModelBinding": "", "Microsoft.Net.Runtime.Interfaces": "0.1-alpha-*" diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 36557a98d8..33df63095e 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Razor; using Microsoft.AspNet.Mvc.Razor.Compilation; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Security.Authorization; namespace Microsoft.AspNet.Mvc { @@ -76,6 +77,8 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Transient(); + yield return describe.Describe( typeof(INestedProviderManager<>), diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTests.cs new file mode 100644 index 0000000000..daad9e792a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTests.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Security.Authorization; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class AuthorizeAttributeTests : AuthorizeAttributeTestsBase + { + [Fact] + public async void Invoke_ValidClaimShouldNotFail() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewPage"); + var authorizationContext = GetAuthorizationContext(services => + services.AddInstance(authorizationService) + ); + + // Act + await authorizeAttribute.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.Null(authorizationContext.Result); + } + + [Fact] + public async void Invoke_EmptyClaimsShouldRejectAnonymousUser() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var authorizeAttribute = new AuthorizeAttribute(); + var authorizationContext = GetAuthorizationContext(services => + services.AddInstance(authorizationService), + anonymous: true + ); + + // Act + await authorizeAttribute.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.NotNull(authorizationContext.Result); + } + + [Fact] + public async void Invoke_EmptyClaimsShouldAuthorizeAuthenticatedUser() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var authorizeAttribute = new AuthorizeAttribute(); + var authorizationContext = GetAuthorizationContext(services => + services.AddInstance(authorizationService) + ); + + // Act + await authorizeAttribute.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.Null(authorizationContext.Result); + } + + [Fact] + public async void Invoke_SingleValidClaimShouldSucceed() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewComment", "CanViewPage"); + var authorizationContext = GetAuthorizationContext(services => + services.AddInstance(authorizationService) + ); + + // Act + await authorizeAttribute.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.Null(authorizationContext.Result); + } + + [Fact] + public async void Invoke_InvalidClaimShouldFail() + { + // Arrange + var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); + var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewComment"); + var authorizationContext = GetAuthorizationContext(services => + services.AddInstance(authorizationService) + ); + + // Act + await authorizeAttribute.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.NotNull(authorizationContext.Result); + } + + [Fact] + public async void Invoke_FailedContextShouldNotCheckPermission() + { + // Arrange + bool authorizationServiceIsCalled = false; + var authorizationService = new Mock(); + authorizationService + .Setup(x => x.AuthorizeAsync(null, null)) + .Returns(() => + { + authorizationServiceIsCalled = true; + return Task.FromResult(true); + }); + + var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewComment"); + var authorizationContext = GetAuthorizationContext(services => + services.AddInstance(authorizationService.Object) + ); + + authorizationContext.Result = new HttpStatusCodeResult(401); + + // Act + await authorizeAttribute.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.False(authorizationServiceIsCalled); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTestsBase.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTestsBase.cs new file mode 100644 index 0000000000..d4e7bf832d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTestsBase.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.DependencyInjection; +using Microsoft.AspNet.DependencyInjection.Fallback; +using Moq; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class AuthorizeAttributeTestsBase + { + protected AuthorizationContext GetAuthorizationContext(Action registerServices, bool anonymous = false) + { + var validUser = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + new Claim(ClaimTypes.Role, "Administrator"), + new Claim(ClaimTypes.NameIdentifier, "John")}, + "Basic")); + + // ServiceProvider + var serviceCollection = new ServiceCollection(); + if (registerServices != null) + { + registerServices(serviceCollection); + } + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + // HttpContext + var httpContext = new Mock(); + httpContext.SetupGet(c => c.User).Returns(anonymous ? null : validUser); + httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider); + + // AuthorizationContext + var actionContext = new ActionContext( + httpContext: httpContext.Object, + router: null, + routeValues: null, + actionDescriptor: null + ); + + var authorizationContext = new AuthorizationContext( + actionContext, + Enumerable.Empty().ToList() + ); + + return authorizationContext; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index e38a508a74..e42800a6ab 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -26,6 +26,8 @@ + +