Skip to content

Commit ac49ef1

Browse files
FrostyApeOneFrostyApeOne
authored andcommitted
Added tenant settings API
1 parent e5d7561 commit ac49ef1

22 files changed

Lines changed: 748 additions & 8 deletions

File tree

src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6533,6 +6533,126 @@ public string BaseUrl
65336533
}
65346534
}
65356535

6536+
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
6537+
/// <summary>
6538+
/// Returns decrypted TenantConfig settings rows for the caller's own tenant.
6539+
/// <br/>Restricted to interactive SuperAdmin users.
6540+
/// </summary>
6541+
/// <returns>Tenant settings.</returns>
6542+
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
6543+
public virtual async System.Threading.Tasks.Task<GetTenantSettingsResponse> GetTenantSettingsAsync(System.Guid tenantId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
6544+
{
6545+
if (tenantId == null)
6546+
throw new System.ArgumentNullException("tenantId");
6547+
6548+
var client_ = _httpClient;
6549+
var disposeClient_ = false;
6550+
try
6551+
{
6552+
using (var request_ = new System.Net.Http.HttpRequestMessage())
6553+
{
6554+
request_.Method = new System.Net.Http.HttpMethod("GET");
6555+
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
6556+
6557+
var urlBuilder_ = new System.Text.StringBuilder();
6558+
if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
6559+
// Operation Path: "v1/admin/tenants/{tenantId}/settings"
6560+
urlBuilder_.Append("v1/admin/tenants/");
6561+
urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(tenantId, System.Globalization.CultureInfo.InvariantCulture)));
6562+
urlBuilder_.Append("/settings");
6563+
6564+
PrepareRequest(client_, request_, urlBuilder_);
6565+
6566+
var url_ = urlBuilder_.ToString();
6567+
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
6568+
6569+
PrepareRequest(client_, request_, url_);
6570+
6571+
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
6572+
var disposeResponse_ = true;
6573+
try
6574+
{
6575+
var headers_ = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>();
6576+
foreach (var item_ in response_.Headers)
6577+
headers_[item_.Key] = item_.Value;
6578+
if (response_.Content != null && response_.Content.Headers != null)
6579+
{
6580+
foreach (var item_ in response_.Content.Headers)
6581+
headers_[item_.Key] = item_.Value;
6582+
}
6583+
6584+
ProcessResponse(client_, response_);
6585+
6586+
var status_ = (int)response_.StatusCode;
6587+
if (status_ == 200)
6588+
{
6589+
var objectResponse_ = await ReadObjectResponseAsync<GetTenantSettingsResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
6590+
if (objectResponse_.Object == null)
6591+
{
6592+
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
6593+
}
6594+
return objectResponse_.Object;
6595+
}
6596+
else
6597+
if (status_ == 401)
6598+
{
6599+
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
6600+
if (objectResponse_.Object == null)
6601+
{
6602+
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
6603+
}
6604+
throw new ExternalApplicationsException<ExceptionResponse>("Unauthorized.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
6605+
}
6606+
else
6607+
if (status_ == 403)
6608+
{
6609+
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
6610+
if (objectResponse_.Object == null)
6611+
{
6612+
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
6613+
}
6614+
throw new ExternalApplicationsException<ExceptionResponse>("Forbidden - interactive SuperAdmin of own tenant required.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
6615+
}
6616+
else
6617+
if (status_ == 404)
6618+
{
6619+
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
6620+
if (objectResponse_.Object == null)
6621+
{
6622+
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
6623+
}
6624+
throw new ExternalApplicationsException<ExceptionResponse>("Tenant not found.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
6625+
}
6626+
else
6627+
if (status_ == 500)
6628+
{
6629+
var objectResponse_ = await ReadObjectResponseAsync<ExceptionResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
6630+
if (objectResponse_.Object == null)
6631+
{
6632+
throw new ExternalApplicationsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
6633+
}
6634+
throw new ExternalApplicationsException<ExceptionResponse>("Internal server error.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
6635+
}
6636+
else
6637+
{
6638+
var responseData_ = response_.Content == null ? null : await ReadAsStringAsync(response_.Content, cancellationToken).ConfigureAwait(false);
6639+
throw new ExternalApplicationsException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
6640+
}
6641+
}
6642+
finally
6643+
{
6644+
if (disposeResponse_)
6645+
response_.Dispose();
6646+
}
6647+
}
6648+
}
6649+
finally
6650+
{
6651+
if (disposeClient_)
6652+
client_.Dispose();
6653+
}
6654+
}
6655+
65366656
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
65376657
/// <summary>
65386658
/// Adds or updates a configuration section for the caller's own tenant only.

src/GovUK.Dfe.FlexForms.Api.Client/Generated/Contracts.g.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,15 @@ public partial interface ITenantAdminClient
427427
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
428428
System.Threading.Tasks.Task<SeedTenantsResponse> SeedFromAppSettingsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
429429

430+
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
431+
/// <summary>
432+
/// Returns decrypted TenantConfig settings rows for the caller's own tenant.
433+
/// <br/>Restricted to interactive SuperAdmin users.
434+
/// </summary>
435+
/// <returns>Tenant settings.</returns>
436+
/// <exception cref="ExternalApplicationsException">A server side error occurred.</exception>
437+
System.Threading.Tasks.Task<GetTenantSettingsResponse> GetTenantSettingsAsync(System.Guid tenantId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
438+
430439
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
431440
/// <summary>
432441
/// Adds or updates a configuration section for the caller's own tenant only.

src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,6 +3500,77 @@
35003500
}
35013501
},
35023502
"/v1/admin/tenants/{tenantId}/settings": {
3503+
"get": {
3504+
"tags": [
3505+
"TenantAdmin"
3506+
],
3507+
"summary": "Returns decrypted TenantConfig settings rows for the caller's own tenant.\nRestricted to interactive SuperAdmin users.",
3508+
"operationId": "TenantAdmin_GetTenantSettings",
3509+
"parameters": [
3510+
{
3511+
"name": "tenantId",
3512+
"in": "path",
3513+
"required": true,
3514+
"schema": {
3515+
"type": "string",
3516+
"format": "guid"
3517+
},
3518+
"x-position": 1
3519+
}
3520+
],
3521+
"responses": {
3522+
"200": {
3523+
"description": "Tenant settings.",
3524+
"content": {
3525+
"application/json": {
3526+
"schema": {
3527+
"$ref": "#/components/schemas/GetTenantSettingsResponse"
3528+
}
3529+
}
3530+
}
3531+
},
3532+
"401": {
3533+
"description": "Unauthorized.",
3534+
"content": {
3535+
"application/json": {
3536+
"schema": {
3537+
"$ref": "#/components/schemas/ExceptionResponse"
3538+
}
3539+
}
3540+
}
3541+
},
3542+
"403": {
3543+
"description": "Forbidden - interactive SuperAdmin of own tenant required.",
3544+
"content": {
3545+
"application/json": {
3546+
"schema": {
3547+
"$ref": "#/components/schemas/ExceptionResponse"
3548+
}
3549+
}
3550+
}
3551+
},
3552+
"404": {
3553+
"description": "Tenant not found.",
3554+
"content": {
3555+
"application/json": {
3556+
"schema": {
3557+
"$ref": "#/components/schemas/ExceptionResponse"
3558+
}
3559+
}
3560+
}
3561+
},
3562+
"500": {
3563+
"description": "Internal server error.",
3564+
"content": {
3565+
"application/json": {
3566+
"schema": {
3567+
"$ref": "#/components/schemas/ExceptionResponse"
3568+
}
3569+
}
3570+
}
3571+
}
3572+
}
3573+
},
35033574
"put": {
35043575
"tags": [
35053576
"TenantAdmin"
@@ -5591,6 +5662,51 @@
55915662
}
55925663
}
55935664
},
5665+
"GetTenantSettingsResponse": {
5666+
"type": "object",
5667+
"additionalProperties": false,
5668+
"properties": {
5669+
"tenantId": {
5670+
"type": "string",
5671+
"format": "guid"
5672+
},
5673+
"tenantName": {
5674+
"type": "string"
5675+
},
5676+
"settings": {
5677+
"type": "array",
5678+
"items": {
5679+
"$ref": "#/components/schemas/TenantSettingDto"
5680+
}
5681+
}
5682+
}
5683+
},
5684+
"TenantSettingDto": {
5685+
"type": "object",
5686+
"additionalProperties": false,
5687+
"properties": {
5688+
"settingId": {
5689+
"type": "string",
5690+
"format": "guid"
5691+
},
5692+
"category": {
5693+
"type": "string"
5694+
},
5695+
"target": {
5696+
"type": "string"
5697+
},
5698+
"settingsJson": {
5699+
"type": "string"
5700+
},
5701+
"isSecret": {
5702+
"type": "boolean"
5703+
},
5704+
"updatedAtUtc": {
5705+
"type": "string",
5706+
"format": "date-time"
5707+
}
5708+
}
5709+
},
55945710
"UpsertTenantSettingResponse": {
55955711
"type": "object",
55965712
"additionalProperties": false,

src/GovUK.Dfe.FlexForms.Api.Client/GovUK.Dfe.FlexForms.Api.Client.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
<ItemGroup>
2121
<FrameworkReference Include="Microsoft.AspNetCore.App" />
22-
<PackageReference Include="GovUK.Dfe.CoreLibs.Contracts" Version="1.0.82" />
22+
<PackageReference Include="GovUK.Dfe.CoreLibs.Contracts" Version="1.0.83" />
2323
<PackageReference Include="GovUK.Dfe.CoreLibs.Http" Version="1.0.10" />
2424
<PackageReference Include="GovUK.Dfe.CoreLibs.Security" Version="1.1.29-prerelease.5" />
2525
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Text.Json;
2+
3+
namespace GovUK.Dfe.FlexForms.Api.Client;
4+
5+
/// <summary>
6+
/// Ensures tenant-admin DTOs deserialize camelCase JSON from the API
7+
/// (same pattern as <see cref="RolesClient"/>).
8+
/// </summary>
9+
public partial class TenantAdminClient
10+
{
11+
static partial void UpdateJsonSerializerSettings(JsonSerializerOptions settings)
12+
{
13+
settings.PropertyNameCaseInsensitive = true;
14+
}
15+
}

src/GovUK.Dfe.FlexForms.Api/Controllers/TenantAdminController.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Asp.Versioning;
2+
using GovUK.Dfe.FlexForms.Application.TenantAdmin;
23
using GovUK.Dfe.FlexForms.Application.TenantAdmin.Commands;
34
using GovUK.Dfe.FlexForms.Application.TenantAdmin.Queries;
45
using GovUK.Dfe.FlexForms.Infrastructure.Security;
@@ -90,6 +91,29 @@ public async Task<IActionResult> SeedFromAppSettings(CancellationToken cancellat
9091
};
9192
}
9293

94+
/// <summary>
95+
/// Returns decrypted TenantConfig settings rows for the caller's own tenant.
96+
/// Restricted to interactive SuperAdmin users.
97+
/// </summary>
98+
[HttpGet("{tenantId:guid}/settings")]
99+
[Authorize(Policy = AuthConstants.TenantAdminUserPolicy)]
100+
[SwaggerResponse(200, "Tenant settings.", typeof(GetTenantSettingsResponse))]
101+
[SwaggerResponse(401, "Unauthorized.", typeof(ExceptionResponse))]
102+
[SwaggerResponse(403, "Forbidden - interactive SuperAdmin of own tenant required.", typeof(ExceptionResponse))]
103+
[SwaggerResponse(404, "Tenant not found.", typeof(ExceptionResponse))]
104+
[SwaggerResponse(500, "Internal server error.", typeof(ExceptionResponse))]
105+
public async Task<IActionResult> GetTenantSettings(
106+
Guid tenantId,
107+
CancellationToken cancellationToken)
108+
{
109+
var result = await sender.Send(new GetTenantSettingsQuery(tenantId), cancellationToken);
110+
111+
if (!result.IsSuccess)
112+
return MapFailure(result);
113+
114+
return new ObjectResult(result) { StatusCode = StatusCodes.Status200OK };
115+
}
116+
93117
/// <summary>
94118
/// Adds or updates a configuration section for the caller's own tenant only.
95119
/// Requires an interactive Admin user JWT; the route <paramref name="tenantId"/> must

src/GovUK.Dfe.FlexForms.Api/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public static async Task Main(string[] args)
117117
builder.Services.AddSingleton<ITenantSettingsEncryptor>(encryptor);
118118
builder.Services.AddScoped<ITenantConfigSeeder, GovUK.Dfe.FlexForms.Infrastructure.Services.TenantConfigSeederService>();
119119
builder.Services.AddScoped<ITenantSettingsWriter, GovUK.Dfe.FlexForms.Infrastructure.Services.TenantSettingsWriterService>();
120+
builder.Services.AddScoped<ITenantSettingsQuery, GovUK.Dfe.FlexForms.Infrastructure.Services.TenantSettingsQueryService>();
120121
builder.Services.AddScoped<ITenantPrincipalResolver, GovUK.Dfe.FlexForms.Infrastructure.Services.TenantPrincipalResolver>();
121122
builder.Services.AddScoped<ITenantSettingsReader, GovUK.Dfe.FlexForms.Infrastructure.Services.TenantSettingsReader>();
122123
builder.Services.AddScoped<ITenantHostnameResolver, GovUK.Dfe.FlexForms.Infrastructure.Services.TenantHostnameResolver>();

src/GovUK.Dfe.FlexForms.Application/Services/ClaimBasedPermissionCheckerService.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ public bool IsInteractiveTenantAdmin()
125125
return PermissionClaimEvaluator.IsInteractiveTenantAdmin(user);
126126
}
127127

128+
/// <inheritdoc />
129+
public bool IsInteractivePlatformAdmin()
130+
{
131+
var user = _httpContextAccessor.HttpContext?.User;
132+
if (user == null) return false;
133+
134+
return PermissionClaimEvaluator.IsInteractivePlatformAdmin(user);
135+
}
136+
128137
/// <inheritdoc />
129138
public bool CanReadAllApplications()
130139
{

src/GovUK.Dfe.FlexForms.Application/TenantAdmin/Commands/UpsertTenantSettingCommandHandler.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public sealed record UpsertTenantSettingCommand(
1919
public sealed class UpsertTenantSettingCommandHandler(
2020
ITenantSettingsWriter settingsWriter,
2121
ITenantContextAccessor tenantContextAccessor,
22-
IPermissionCheckerService permissionChecker)
22+
IPermissionCheckerService permissionChecker,
23+
ITenantConfigurationProvider tenantConfigProvider)
2324
: IRequestHandler<UpsertTenantSettingCommand, Result<UpsertTenantSettingResponse>>
2425
{
2526
public async Task<Result<UpsertTenantSettingResponse>> Handle(
@@ -56,6 +57,8 @@ public async Task<Result<UpsertTenantSettingResponse>> Handle(
5657
request.IsSecret,
5758
cancellationToken);
5859

60+
await tenantConfigProvider.RefreshAsync(cancellationToken);
61+
5962
var verb = result.WasCreated ? "created" : "updated";
6063

6164
return Result<UpsertTenantSettingResponse>.Success(

0 commit comments

Comments
 (0)