Move directories, rename controllers

This commit is contained in:
nicolas.dorier
2020-03-27 12:58:45 +09:00
parent ac14f199e4
commit afdee9d8a2
9 changed files with 358 additions and 3 deletions

View File

@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Security.GreenField;
namespace BTCPayServer.Controllers.RestApi
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]

View File

@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.RestApi
namespace BTCPayServer.Controllers.GreenField
{
/// <summary>
/// this controller serves as a testing endpoint for our api key unit tests

View File

@@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using NicolasDorier.RateLimits;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers.RestApi
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]

View File

@@ -0,0 +1,64 @@
using System;
using System.Linq;
using BTCPayServer.Client;
using BTCPayServer.Security.Bitpay;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
namespace BTCPayServer.Security.GreenField
{
public static class APIKeyExtensions
{
public static bool GetAPIKey(this HttpContext httpContext, out StringValues apiKey)
{
if (httpContext.Request.Headers.TryGetValue("Authorization", out var value) &&
value.ToString().StartsWith("token ", StringComparison.InvariantCultureIgnoreCase))
{
apiKey = value.ToString().Substring("token ".Length);
return true;
}
return false;
}
public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder)
{
builder.AddScheme<GreenFieldAuthenticationOptions, GreenFieldAuthenticationHandler>(AuthenticationSchemes.Greenfield,
o => { });
return builder;
}
public static IServiceCollection AddAPIKeyAuthentication(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<APIKeyRepository>();
serviceCollection.AddScoped<IAuthorizationHandler, GreenFieldAuthorizationHandler>();
return serviceCollection;
}
public static string[] GetPermissions(this AuthorizationHandlerContext context)
{
return context.User.Claims.Where(c =>
c.Type.Equals(GreenFieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase))
.Select(claim => claim.Value).ToArray();
}
public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission)
{
foreach (var claim in context.User.Claims.Where(c =>
c.Type.Equals(GreenFieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
{
if (Permission.TryParse(claim.Value, out var claimPermission))
{
if (claimPermission.Contains(permission))
{
return true;
}
}
}
return false;
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Security.GreenField
{
public class APIKeyRepository
{
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
public APIKeyRepository(ApplicationDbContextFactory applicationDbContextFactory)
{
_applicationDbContextFactory = applicationDbContextFactory;
}
public async Task<APIKeyData> GetKey(string apiKey)
{
using (var context = _applicationDbContextFactory.CreateContext())
{
return await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys,
data => data.Id == apiKey && data.Type != APIKeyType.Legacy);
}
}
public async Task<List<APIKeyData>> GetKeys(APIKeyQuery query)
{
using (var context = _applicationDbContextFactory.CreateContext())
{
var queryable = context.ApiKeys.AsQueryable();
if (query?.UserId != null && query.UserId.Any())
{
queryable = queryable.Where(data => query.UserId.Contains(data.UserId));
}
return await queryable.ToListAsync();
}
}
public async Task CreateKey(APIKeyData key)
{
if (key.Type == APIKeyType.Legacy || !string.IsNullOrEmpty(key.StoreId) || string.IsNullOrEmpty(key.UserId))
{
throw new InvalidOperationException("cannot save a bitpay legacy api key with this repository");
}
using (var context = _applicationDbContextFactory.CreateContext())
{
await context.ApiKeys.AddAsync(key);
await context.SaveChangesAsync();
}
}
public async Task Remove(string id, string getUserId)
{
using (var context = _applicationDbContextFactory.CreateContext())
{
var key = await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys,
data => data.Id == id && data.UserId == getUserId);
context.ApiKeys.Remove(key);
await context.SaveChangesAsync();
}
}
public class APIKeyQuery
{
public string[] UserId { get; set; }
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Security.Bitpay;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Security.GreenField
{
public class GreenFieldAuthenticationHandler : AuthenticationHandler<GreenFieldAuthenticationOptions>
{
private readonly APIKeyRepository _apiKeyRepository;
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public GreenFieldAuthenticationHandler(
APIKeyRepository apiKeyRepository,
IOptionsMonitor<IdentityOptions> identityOptions,
IOptionsMonitor<GreenFieldAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) : base(options, logger, encoder, clock)
{
_apiKeyRepository = apiKeyRepository;
_identityOptions = identityOptions;
_signInManager = signInManager;
_userManager = userManager;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var res = await HandleApiKeyAuthenticateResult();
if (res.None)
{
return await HandleBasicAuthenticateAsync();
}
return res;
}
private async Task<AuthenticateResult> HandleApiKeyAuthenticateResult()
{
if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey))
return AuthenticateResult.NoResult();
var key = await _apiKeyRepository.GetKey(apiKey);
if (key == null)
{
return AuthenticateResult.Fail("ApiKey authentication failed");
}
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId));
claims.AddRange(Permission.ToPermissions(key.Permissions).Select(permission =>
new Claim(GreenFieldConstants.ClaimTypes.Permission, permission.ToString())));
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(claims, GreenFieldConstants.AuthenticationType)),
GreenFieldConstants.AuthenticationType));
}
private async Task<AuthenticateResult> HandleBasicAuthenticateAsync()
{
string authHeader = Context.Request.Headers["Authorization"];
if (authHeader == null || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) return AuthenticateResult.NoResult();
var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
var decodedUsernamePassword =
Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)).Split(':');
var username = decodedUsernamePassword[0];
var password = decodedUsernamePassword[1];
var result = await _signInManager.PasswordSignInAsync(username, password, true, true);
if (!result.Succeeded) return AuthenticateResult.Fail(result.ToString());
var user = await _userManager.FindByNameAsync(username);
var claims = new List<Claim>()
{
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id),
new Claim(GreenFieldConstants.ClaimTypes.Permission,
Permission.Create(Policies.Unrestricted).ToString())
};
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(claims, GreenFieldConstants.AuthenticationType)),
GreenFieldConstants.AuthenticationType));
}
}
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace BTCPayServer.Security.GreenField
{
public class GreenFieldAuthenticationOptions : AuthenticationSchemeOptions
{
}
}

View File

@@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
namespace BTCPayServer.Security.GreenField
{
public class GreenFieldAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
{
private readonly HttpContext _HttpContext;
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
public GreenFieldAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository)
{
_HttpContext = httpContextAccessor.HttpContext;
_userManager = userManager;
_storeRepository = storeRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PolicyRequirement requirement)
{
if (context.User.Identity.AuthenticationType != GreenFieldConstants.AuthenticationType)
return;
bool success = false;
switch (requirement.Policy)
{
case Policies.CanModifyProfile:
case Policies.CanViewProfile:
case Policies.Unrestricted:
success = context.HasPermission(Permission.Create(requirement.Policy));
break;
case Policies.CanViewStoreSettings:
case Policies.CanModifyStoreSettings:
var storeId = _HttpContext.GetImplicitStoreId();
var userid = _userManager.GetUserId(context.User);
// Specific store action
if (storeId != null)
{
if (context.HasPermission(Permission.Create(requirement.Policy, storeId)))
{
if (string.IsNullOrEmpty(userid))
break;
var store = await _storeRepository.FindStore((string)storeId, userid);
if (store == null)
break;
success = true;
_HttpContext.SetStoreData(store);
}
}
else
{
var stores = await _storeRepository.GetStoresByUserId(userid);
List<StoreData> permissionedStores = new List<StoreData>();
foreach (var store in stores)
{
if (context.HasPermission(Permission.Create(requirement.Policy, store.Id)))
permissionedStores.Add(store);
}
_HttpContext.SetStoresData(stores.ToArray());
success = true;
}
break;
case Policies.CanCreateUser:
case Policies.CanModifyServerSettings:
if (context.HasPermission(Permission.Create(requirement.Policy)))
{
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
break;
if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
break;
success = true;
}
break;
}
if (success)
{
context.Succeed(requirement);
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using BTCPayServer.Client;
namespace BTCPayServer.Security.GreenField
{
public static class GreenFieldConstants
{
public const string AuthenticationType = "GreenField";
public static class ClaimTypes
{
public const string Permission = "APIKey.Permission";
}
}
}