Skip to content

Commit

Permalink
Api key management (#615)
Browse files Browse the repository at this point in the history
* start and create api keys -screen

* Added revoked column

* Added migration for new column

* Added repository method to get list of Api keys

* Added model

* Added use case to get Api keys based on Organisation id

* Added use case to create new api key

* Added test to register authentication key

* Added Register Authentication key endpoint in api

* updated return type of endpoints

* Added Patch endpoint to revoke authentication key
Added repo method
Added test for repo & usecase

* Added revoke unctionality
Api key listing
unit test

* APIKey Name already exist check while saving

* Added authorize policy for editor role

* PR changes for api endpoint

* Added new column and made revoved non-nullable

* Fix migration

* Added auth key revoke consideration

* used RevokedOn instead of UpdatedOn

---------

Co-authored-by: Dharm <[email protected]>
  • Loading branch information
dpatel017 and dharmverma authored Sep 23, 2024
1 parent 6aaf5b3 commit 8708d07
Show file tree
Hide file tree
Showing 38 changed files with 3,175 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using Amazon.S3;
using CO.CDP.Organisation.WebApiClient;
using CO.CDP.OrganisationApp.Constants;
using CO.CDP.OrganisationApp.Pages.ApiKeyManagement;
using FluentAssertions;
using FluentAssertions.Equivalency;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Moq;
using System.Net;
using ProblemDetails = CO.CDP.Organisation.WebApiClient.ProblemDetails;

namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement;

public class CreateApiKeyTest
{
private readonly Mock<IOrganisationClient> _mockOrganisationClient = new();
private readonly Guid _organisationId = Guid.NewGuid();
private readonly CreateApiKeyModel _model;
public CreateApiKeyTest()
{
_model = new CreateApiKeyModel(_mockOrganisationClient.Object);
}

[Fact]
public async Task OnPost_WhenModelStateIsInvalid_ShouldReturnPage()
{
_model.ModelState.AddModelError("key", "error");

var result = await _model.OnPost();

result.Should().BeOfType<PageResult>();
}

[Fact]
public async Task OnPost_WhenApiExceptionOccurs_ShouldRedirectToPageNotFound()
{
_mockOrganisationClient.Setup(client => client.CreateAuthenticationKeyAsync(It.IsAny<Guid>(), It.IsAny<RegisterAuthenticationKey>()))
.ThrowsAsync(new ApiException(string.Empty, (int)HttpStatusCode.NotFound, string.Empty, null, null));

_model.Id = Guid.NewGuid();
_model.ApiKeyName = "TestApiKey";

var result = await _model.OnPost();

result.Should().BeOfType<RedirectResult>()
.Which.Url.Should().Be("/page-not-found");
}

[Fact]
public async Task OnPost_ShouldRedirectToManageApiKey()
{
_model.Id = Guid.NewGuid();

_mockOrganisationClient
.Setup(c => c.CreateAuthenticationKeyAsync(_organisationId, DummyApiKeyEntity()));

var result = await _model.OnPost();

var redirectToPageResult = result.Should().BeOfType<RedirectToPageResult>().Subject;

_mockOrganisationClient.Verify(c => c.CreateAuthenticationKeyAsync(It.IsAny<Guid>(), It.IsAny<RegisterAuthenticationKey>()), Times.Once());

redirectToPageResult.PageName.Should().Be("NewApiKeyDetails");
}

[Fact]
public async Task OnPost_ShouldReturnPage_WhenCreateApiKeyModelStateIsInvalid()
{
var model = new CreateApiKeyModel(_mockOrganisationClient.Object);

model.ModelState.AddModelError("ApiKeyName", "Enter the api key name");

var result = await model.OnPost();

result.Should().BeOfType<PageResult>();
}

[Theory]
[InlineData(ErrorCodes.APIKEY_NAME_ALREADY_EXISTS, ErrorMessagesList.DuplicateApiKeyName, StatusCodes.Status400BadRequest)]
public async Task OnPost_AddsModelError(string errorCode, string expectedErrorMessage, int statusCode)
{
var problemDetails = new ProblemDetails(
title: errorCode,
detail: ErrorMessagesList.DuplicateApiKeyName,
status: statusCode,
instance: null,
type: null
)
{
AdditionalProperties =
{
{ "code", "APIKEY_NAME_ALREADY_EXISTS" }
}
};
var aex = new ApiException<ProblemDetails>(
"Duplicate Api key name",
statusCode,
"Bad Request",
null,
problemDetails ?? new ProblemDetails("Detail", "Instance", statusCode, "Problem title", "Problem type"),
null
);

_mockOrganisationClient.Setup(client => client.CreateAuthenticationKeyAsync(It.IsAny<Guid>(), It.IsAny<RegisterAuthenticationKey>()))
.ThrowsAsync(aex);

_model.Id = Guid.NewGuid();
_model.ApiKeyName = "TestApiKey";

var result = await _model.OnPost();

_model.ModelState[string.Empty].As<ModelStateEntry>().Errors
.Should().Contain(e => e.ErrorMessage == expectedErrorMessage);
}

private RegisterAuthenticationKey DummyApiKeyEntity()
{
return new RegisterAuthenticationKey(key: "_key", name: "name", organisationId: _organisationId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using CO.CDP.Organisation.WebApiClient;
using CO.CDP.OrganisationApp.Pages.ApiKeyManagement;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Moq;
using System.Net;

namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement;

public class ManageApiKeyTest
{
private readonly Mock<IOrganisationClient> _mockOrganisationClient = new();
private readonly ManageApiKeyModel _model;

public ManageApiKeyTest()
{
_model = new ManageApiKeyModel(_mockOrganisationClient.Object);
_model.Id = Guid.NewGuid();
}

[Fact]
public async Task OnGet_WhenAuthenticationKeysAreRetrieved_ShouldReturnPage()
{
var authenticationKeys = new List<AuthenticationKey> {
new AuthenticationKey(createdOn: DateTimeOffset.UtcNow.AddDays(-1), name: "TestKey1", revoked: false, revokedOn: DateTimeOffset.UtcNow)
};

_mockOrganisationClient
.Setup(client => client.GetAuthenticationKeysAsync(It.IsAny<Guid>()))
.ReturnsAsync(authenticationKeys);

var result = await _model.OnGet();

result.Should().BeOfType<PageResult>();

_model.AuthenticationKeys.Should().HaveCount(1);
_model.AuthenticationKeys.FirstOrDefault()!.Name.Should().Be("TestKey1");
}

[Fact]
public async Task OnGet_WhenApiExceptionOccurs_ShouldRedirectToPageNotFound()
{
_mockOrganisationClient.Setup(client => client.GetAuthenticationKeysAsync(It.IsAny<Guid>()))
.ThrowsAsync(new ApiException(string.Empty, (int)HttpStatusCode.NotFound, string.Empty, null, null));

var result = await _model.OnGet();

result.Should().BeOfType<RedirectResult>()
.Which.Url.Should().Be("/page-not-found");
}

[Fact]
public void OnPost_ShouldRedirectToCreateApiKeyPage()
{
var result = _model.OnPost();

result.Should().BeOfType<RedirectToPageResult>()
.Which.PageName.Should().Be("CreateApiKey");

result.As<RedirectToPageResult>().RouteValues!["Id"].Should().Be(_model.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using CO.CDP.OrganisationApp.Pages.ApiKeyManagement;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement;

public class NewApiKeyDetailsTest
{
[Fact]
public void OnGet_ShouldReturnPageResult()
{
var pageModel = new NewApiKeyDetailsModel
{
Id = Guid.NewGuid(),
ApiKey = "test-api-key"
};

var result = pageModel.OnGet();

result.Should().BeOfType<PageResult>();
pageModel.Id.Should().NotBeEmpty();
pageModel.ApiKey.Should().Be("test-api-key");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using CO.CDP.Organisation.WebApiClient;
using CO.CDP.OrganisationApp.Pages.ApiKeyManagement;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Moq;
using System.Net;

namespace CO.CDP.OrganisationApp.Tests.Pages.ApiKeyManagement;

public class RevokeApiKeyTest
{
private readonly Mock<IOrganisationClient> _mockOrganisationClient;
private readonly RevokeApiKeyModel _pageModel;

public RevokeApiKeyTest()
{
_mockOrganisationClient = new Mock<IOrganisationClient>();
_pageModel = new RevokeApiKeyModel(_mockOrganisationClient.Object)
{
Id = Guid.NewGuid(),
ApiKeyName = "TestApiKey"
};
}

[Fact]
public async Task OnPost_ValidModelState_ShouldRedirectToManageApiKeyPage()
{
_mockOrganisationClient
.Setup(client => client.RevokeAuthenticationKeyAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.Returns(Task.CompletedTask);

var result = await _pageModel.OnPost();

result.Should().BeOfType<RedirectToPageResult>()
.Which.PageName.Should().Be("ManageApiKey");
result.As<RedirectToPageResult>().RouteValues!["Id"].Should().Be(_pageModel.Id);

_mockOrganisationClient.Verify(client => client.RevokeAuthenticationKeyAsync(_pageModel.Id, _pageModel.ApiKeyName), Times.Once);
}

[Fact]
public async Task OnPost_InvalidModelState_ShouldReturnPageResult()
{
_pageModel.ModelState.AddModelError("ApiKeyName", "ApiKeyName is required");

var result = await _pageModel.OnPost();

result.Should().BeOfType<PageResult>();

_mockOrganisationClient.Verify(client => client.RevokeAuthenticationKeyAsync(It.IsAny<Guid>(), It.IsAny<string>()), Times.Never);
}

[Fact]
public async Task OnPost_ApiException404_ShouldRedirectToPageNotFound()
{
_mockOrganisationClient.Setup(client => client.RevokeAuthenticationKeyAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new ApiException(string.Empty, (int)HttpStatusCode.NotFound, string.Empty, null, null));

var result = await _pageModel.OnPost();

result.Should().BeOfType<RedirectResult>()
.Which.Url.Should().Be("/page-not-found");

_mockOrganisationClient.Verify(client => client.RevokeAuthenticationKeyAsync(_pageModel.Id, _pageModel.ApiKeyName), Times.Once);
}
}
1 change: 1 addition & 0 deletions Frontend/CO.CDP.OrganisationApp/Constants/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ public static class ErrorCodes
public const string BUYER_INFO_NOT_EXISTS = "BUYER_INFO_NOT_EXISTS";
public const string UNKNOWN_BUYER_INFORMATION_UPDATE_TYPE = "UNKNOWN_BUYER_INFORMATION_UPDATE_TYPE";
public const string PERSON_INVITE_ALREADY_CLAIMED = "PERSON_INVITE_ALREADY_CLAIMED";
public const string APIKEY_NAME_ALREADY_EXISTS = "APIKEY_NAME_ALREADY_EXISTS";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public static class ErrorMessagesList
public const string BuyerInfoNotExists = "The buyer information for requested organisation not exist.";
public const string UnknownBuyerInformationUpdateType = "The requested buyer information update type is unknown.";

public const string DuplicateApiKeyName = "An API key Name with this name already exists. Please try again.";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@page "/organisation/{id}/manage-api-key/create"
@model CreateApiKeyModel

@{
var apiKeyNameHasError = ((TagBuilder)Html.ValidationMessageFor(m => m.ApiKeyName)).HasInnerHtml;
}
<a href="/organisation/@Model.Id/supplier-information/manage-api-key/start" class="govuk-back-link">Back</a>

<main class="govuk-main-wrapper">
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<partial name="_ErrorSummary" model="@ModelState" />
<form class="form" method="post">
<div class="govuk-form-group">
<h1 class="govuk-label-wrapper">
<label class="govuk-label govuk-label--l" for="APIKeyName">
Name your API key
</label>
</h1>
<div id="hint" class="govuk-hint">
For security we only show the API key once on the confirmation page. Add a name for reference. For example, the name of the eSender it's for.
</div>
<div class="govuk-form-group @(apiKeyNameHasError ? "govuk-form-group--error" : "")">
@if (apiKeyNameHasError)
{
<p id="api-key-name-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span>
@Html.ValidationMessageFor(m => m.ApiKeyName)
</p>
}
<input class="govuk-input govuk-!-width-two-thirds @(apiKeyNameHasError ? "govuk-input--error" : "")"
id="@nameof(Model.ApiKeyName)" value="@Model.ApiKeyName" name="@nameof(Model.ApiKeyName)" type="text"
@(apiKeyNameHasError ? "aria-describedby=api-key-name-error" : "")>

</div>
</div>

<div class="govuk-button-group govuk-!-margin-top-6">
<button class="govuk-button" data-module="govuk-button" type="submit">
Continue
</button>
</div>
</form>
</div>
</div>
</main>
Loading

0 comments on commit 8708d07

Please sign in to comment.