-
-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1303 from json-api-dotnet/auth-scopes-example
Add example for scopes-based authorization
- Loading branch information
Showing
16 changed files
with
1,664 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/Actor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using JetBrains.Annotations; | ||
using JsonApiDotNetCore.Resources; | ||
using JsonApiDotNetCore.Resources.Annotations; | ||
|
||
namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes; | ||
|
||
[UsedImplicitly(ImplicitUseTargetFlags.Members)] | ||
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes")] | ||
public sealed class Actor : Identifiable<long> | ||
{ | ||
[Attr] | ||
public string Name { get; set; } = null!; | ||
|
||
[Attr] | ||
public DateTime BornAt { get; set; } | ||
|
||
[HasMany] | ||
public ISet<Movie> ActsIn { get; set; } = new HashSet<Movie>(); | ||
} |
127 changes: 127 additions & 0 deletions
127
test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/AuthScopeSet.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
using System.Text; | ||
using JsonApiDotNetCore.Configuration; | ||
using JsonApiDotNetCore.Middleware; | ||
using JsonApiDotNetCore.Resources; | ||
using JsonApiDotNetCore.Resources.Annotations; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes; | ||
|
||
internal sealed class AuthScopeSet | ||
{ | ||
private const StringSplitOptions ScopesHeaderSplitOptions = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; | ||
|
||
public const string ScopesHeaderName = "X-Auth-Scopes"; | ||
|
||
private readonly Dictionary<string, Permission> _scopes = new(); | ||
|
||
public static AuthScopeSet GetRequestedScopes(IHeaderDictionary requestHeaders) | ||
{ | ||
var requestedScopes = new AuthScopeSet(); | ||
|
||
// In a real application, the scopes would be read from the signed ticket in the Authorization HTTP header. | ||
// For simplicity, this sample allows the client to send them directly, which is obviously insecure. | ||
|
||
if (requestHeaders.TryGetValue(ScopesHeaderName, out StringValues headerValue)) | ||
{ | ||
foreach (string scopeValue in headerValue.ToString().Split(' ', ScopesHeaderSplitOptions)) | ||
{ | ||
string[] scopeParts = scopeValue.Split(':', 2, ScopesHeaderSplitOptions); | ||
|
||
if (scopeParts.Length == 2 && Enum.TryParse(scopeParts[0], true, out Permission permission) && Enum.IsDefined(permission)) | ||
{ | ||
requestedScopes.Include(scopeParts[1], permission); | ||
} | ||
} | ||
} | ||
|
||
return requestedScopes; | ||
} | ||
|
||
public void IncludeFrom(IJsonApiRequest request, ITargetedFields targetedFields) | ||
{ | ||
Permission permission = request.IsReadOnly ? Permission.Read : Permission.Write; | ||
|
||
if (request.PrimaryResourceType != null) | ||
{ | ||
Include(request.PrimaryResourceType, permission); | ||
} | ||
|
||
if (request.SecondaryResourceType != null) | ||
{ | ||
Include(request.SecondaryResourceType, permission); | ||
} | ||
|
||
if (request.Relationship != null) | ||
{ | ||
Include(request.Relationship, permission); | ||
} | ||
|
||
foreach (RelationshipAttribute relationship in targetedFields.Relationships) | ||
{ | ||
Include(relationship, permission); | ||
} | ||
} | ||
|
||
public void Include(ResourceType resourceType, Permission permission) | ||
{ | ||
Include(resourceType.PublicName, permission); | ||
} | ||
|
||
public void Include(RelationshipAttribute relationship, Permission permission) | ||
{ | ||
Include(relationship.LeftType, permission); | ||
Include(relationship.RightType, permission); | ||
} | ||
|
||
private void Include(string name, Permission permission) | ||
{ | ||
// Unify with existing entries. For example, adding read:movies when write:movies already exists is a no-op. | ||
|
||
if (_scopes.TryGetValue(name, out Permission value)) | ||
{ | ||
if (value >= permission) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
_scopes[name] = permission; | ||
} | ||
|
||
public bool ContainsAll(AuthScopeSet other) | ||
{ | ||
foreach (string otherName in other._scopes.Keys) | ||
{ | ||
if (!_scopes.TryGetValue(otherName, out Permission thisPermission)) | ||
{ | ||
return false; | ||
} | ||
|
||
if (thisPermission < other._scopes[otherName]) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public override string ToString() | ||
{ | ||
var builder = new StringBuilder(); | ||
|
||
foreach ((string name, Permission permission) in _scopes.OrderBy(scope => scope.Key)) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(' '); | ||
} | ||
|
||
builder.Append($"{permission.ToString().ToLowerInvariant()}:{name}"); | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/Genre.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using JetBrains.Annotations; | ||
using JsonApiDotNetCore.Resources; | ||
using JsonApiDotNetCore.Resources.Annotations; | ||
|
||
namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes; | ||
|
||
[UsedImplicitly(ImplicitUseTargetFlags.Members)] | ||
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes")] | ||
public sealed class Genre : Identifiable<long> | ||
{ | ||
[Attr] | ||
public string Name { get; set; } = null!; | ||
|
||
[HasMany] | ||
public ISet<Movie> Movies { get; set; } = new HashSet<Movie>(); | ||
} |
25 changes: 25 additions & 0 deletions
25
test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/Movie.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using JetBrains.Annotations; | ||
using JsonApiDotNetCore.Resources; | ||
using JsonApiDotNetCore.Resources.Annotations; | ||
|
||
namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes; | ||
|
||
[UsedImplicitly(ImplicitUseTargetFlags.Members)] | ||
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes")] | ||
public sealed class Movie : Identifiable<long> | ||
{ | ||
[Attr] | ||
public string Title { get; set; } = null!; | ||
|
||
[Attr] | ||
public int ReleaseYear { get; set; } | ||
|
||
[Attr] | ||
public int DurationInSeconds { get; set; } | ||
|
||
[HasOne] | ||
public Genre Genre { get; set; } = null!; | ||
|
||
[HasMany] | ||
public ISet<Actor> Cast { get; set; } = new HashSet<Actor>(); | ||
} |
53 changes: 53 additions & 0 deletions
53
test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/OperationsController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using System.Net; | ||
using JsonApiDotNetCore.AtomicOperations; | ||
using JsonApiDotNetCore.Configuration; | ||
using JsonApiDotNetCore.Controllers; | ||
using JsonApiDotNetCore.Middleware; | ||
using JsonApiDotNetCore.Resources; | ||
using JsonApiDotNetCore.Serialization.Objects; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes; | ||
|
||
public sealed class OperationsController : JsonApiOperationsController | ||
{ | ||
public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, | ||
IJsonApiRequest request, ITargetedFields targetedFields) | ||
: base(options, resourceGraph, loggerFactory, processor, request, targetedFields) | ||
{ | ||
} | ||
|
||
public override async Task<IActionResult> PostOperationsAsync(IList<OperationContainer> operations, CancellationToken cancellationToken) | ||
{ | ||
AuthScopeSet requestedScopes = AuthScopeSet.GetRequestedScopes(HttpContext.Request.Headers); | ||
AuthScopeSet requiredScopes = GetRequiredScopes(operations); | ||
|
||
if (!requestedScopes.ContainsAll(requiredScopes)) | ||
{ | ||
return Error(new ErrorObject(HttpStatusCode.Unauthorized) | ||
{ | ||
Title = "Insufficient permissions to perform this request.", | ||
Detail = $"Performing this request requires the following scopes: {requiredScopes}.", | ||
Source = new ErrorSource | ||
{ | ||
Header = AuthScopeSet.ScopesHeaderName | ||
} | ||
}); | ||
} | ||
|
||
return await base.PostOperationsAsync(operations, cancellationToken); | ||
} | ||
|
||
private AuthScopeSet GetRequiredScopes(IEnumerable<OperationContainer> operations) | ||
{ | ||
var requiredScopes = new AuthScopeSet(); | ||
|
||
foreach (OperationContainer operation in operations) | ||
{ | ||
requiredScopes.IncludeFrom(operation.Request, operation.TargetedFields); | ||
} | ||
|
||
return requiredScopes; | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/Permission.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace JsonApiDotNetCoreTests.IntegrationTests.Authorization.Scopes; | ||
|
||
internal enum Permission | ||
{ | ||
Read, | ||
|
||
// Write access implicitly includes read access, because POST/PATCH in JSON:API may return the changed resource. | ||
Write | ||
} |
Oops, something went wrong.