From 39a8c3fe473ef4323a6db4af913f6bae2cf1ab52 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 27 Mar 2020 14:17:31 +0900 Subject: [PATCH] GreenField: Create API Key --- .../BTCPayServerClient.APIKeys.cs | 11 ++- BTCPayServer.Tests/GreenfieldAPITests.cs | 27 +++++++ BTCPayServer.Tests/TestAccount.cs | 6 ++ BTCPayServer/BTCPayServer.csproj | 2 +- .../GreenField/ApiKeysController.cs | 22 +++++- .../wwwroot/swagger/v1/swagger.template.json | 79 ++++++++++++++++++- 6 files changed, 143 insertions(+), 4 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs index dcced301b..756ceaf3f 100644 --- a/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs +++ b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs @@ -1,3 +1,4 @@ +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -12,7 +13,15 @@ namespace BTCPayServer.Client var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current"), token); return await HandleResponse(response); } - + + public virtual async Task CreateAPIKey(CreateApiKeyRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys", bodyPayload: request, method: HttpMethod.Post), token); + return await HandleResponse(response); + } + public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default) { var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 9f83f6d5e..9d9572906 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -55,6 +55,33 @@ namespace BTCPayServer.Tests await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo()); } } + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task CanCreateAPIKeyViaAPI() + { + using (var tester = ServerTester.Create()) + { + await tester.StartAsync(); + var acc = tester.NewAccount(); + await acc.GrantAccessAsync(); + var unrestricted = await acc.CreateClient(); + var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest() + { + Label = "Hello world", + Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } + }); + Assert.Equal("Hello world", apiKey.Label); + var p = Assert.Single(apiKey.Permissions); + Assert.Equal(Policies.CanViewProfile, p.Policy); + + var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey); + await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest() + { + Label = "Hello world2", + Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) } + })); + } + } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 4332ffa33..78d0d7208 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -87,6 +87,12 @@ namespace BTCPayServer.Tests Assert.IsType(await store.RequestPairing(pairingCode.ToString())); await store.Pair(pairingCode.ToString(), StoreId); } + + public BTCPayServerClient CreateClientFromAPIKey(string apiKey) + { + return new BTCPayServerClient(parent.PayTester.ServerUri, apiKey); + } + public void CreateStore() { CreateStoreAsync().GetAwaiter().GetResult(); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index e39898639..47a0c18c3 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -223,5 +223,5 @@ <_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" /> - + diff --git a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs index f272eeada..06165a945 100644 --- a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs +++ b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using BTCPayServer.Security.GreenField; +using NBitcoin.DataEncoders; +using NBitcoin; namespace BTCPayServer.Controllers.GreenField { @@ -35,9 +37,27 @@ namespace BTCPayServer.Controllers.GreenField return Ok(FromModel(data)); } + [HttpPost("~/api/v1/api-keys")] + [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public async Task> CreateKey(CreateApiKeyRequest request) + { + if (request is null) + return BadRequest(); + var key = new APIKeyData() + { + Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)), + Type = APIKeyType.Permanent, + UserId = _userManager.GetUserId(User), + Label = request.Label + }; + key.Permissions = string.Join(";", request.Permissions.Select(p => p.ToString()).Distinct().ToArray()); + await _apiKeyRepository.CreateKey(key); + return Ok(FromModel(key)); + } + [HttpDelete("~/api/v1/api-keys/current")] [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)] - public async Task> RevokeKey() + public async Task RevokeKey() { if (!ControllerContext.HttpContext.GetAPIKey(out var apiKey)) { diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 0e11854ca..fb1f29a1e 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -77,7 +77,7 @@ } } }, - "security": [ ] + "security": [] } }, "/api/v1/users/me": { @@ -237,6 +237,83 @@ } ] } + }, + "/api/v1/api-keys": { + "post": { + "tags": [ + "API Keys" + ], + "summary": "Create a new API Key", + "description": "Create a new API Key", + "responses": { + "200": { + "description": "Information about the new api key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "description": "The label of the new API Key", + "nullable": true + }, + "permissions": { + "type": "array", + "description": "The permissions granted to this API Key (See API Key Authentication)", + "nullable": true, + "items": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "API Key": [ "unrestricted" ], + "Basic": [] + } + ] + }, + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Revoke the current API Key", + "description": "Revoke the current API key so that it cannot be used anymore", + "responses": { + "200": { + "description": "The key was revoked and is no longer usable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "API Key": [ "unrestricted" ] + } + ] + } } }, "components": {