Skip to content

Commit ead1fb9

Browse files
committed
Implemented Outbox on Requests
1 parent 0e3a569 commit ead1fb9

File tree

9 files changed

+430
-29
lines changed

9 files changed

+430
-29
lines changed

src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/RequestController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Altinn.AccessMgmt.Core.Audit;
88
using Altinn.AccessMgmt.Core.Services;
99
using Altinn.AccessMgmt.Core.Services.Contracts;
10+
using Altinn.AccessMgmt.Core.Utils;
1011
using Altinn.AccessMgmt.PersistenceEF.Constants;
1112
using Altinn.AccessMgmt.PersistenceEF.Migrations;
1213
using Altinn.AccessMgmt.PersistenceEF.Queries.Connection;
@@ -140,8 +141,8 @@ public async Task<IActionResult> CreateRequest([FromQuery] Guid party, [FromQuer
140141
To = party,
141142
Role = role.Id,
142143
Status = status,
143-
Resource = resource?.Id,
144-
Package = package?.Id,
144+
Resource = DtoMapper.Convert(resource),
145+
Package = package,
145146
},
146147
ct
147148
);

src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.ServiceOwner/Controllers/RequestController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Altinn.AccessMgmt.Core;
55
using Altinn.AccessMgmt.Core.Audit;
66
using Altinn.AccessMgmt.Core.Services.Contracts;
7+
using Altinn.AccessMgmt.Core.Utils;
78
using Altinn.AccessMgmt.PersistenceEF.Constants;
89
using Altinn.AccessMgmt.PersistenceEF.Models;
910
using Altinn.Authorization.Api.Contracts.AccessManagement.Request;
@@ -111,8 +112,8 @@ public async Task<IActionResult> CreateRequest([FromBody] CreateServiceOwnerRequ
111112
To = to.Id,
112113
Role = role.Id,
113114
Status = status,
114-
Resource = resource?.Id,
115-
Package = package?.Id,
115+
Resource = DtoMapper.Convert(resource),
116+
Package = package,
116117
},
117118
ct
118119
);

src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/AccessManagementHost.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ public static WebApplication Create(string[] args)
104104
options.Source = appsettings.RunInitOnly ? SourceType.Migration : SourceType.App;
105105
options.AddOutboxHandler<ResourceRequestAcceptedNotificationHandler>("resource_request_accepted");
106106
options.AddOutboxHandler<ResourceRequestPendingNotificationHandler>("resource_request_pending");
107+
options.AddOutboxHandler<PackageRequestAcceptedNotificationHandler>("package_request_accepted");
108+
options.AddOutboxHandler<PackageRequestPendingNotificationHandler>("package_request_pending");
107109
});
108110

109111
builder.Services.AddAccessMgmtCore(builder.Configuration);

src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Core/Extensions/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public static IServiceCollection AddAccessMgmtCore(this IServiceCollection servi
5353

5454
services.AddTransient<ResourceRequestAcceptedNotificationHandler>();
5555
services.AddTransient<ResourceRequestPendingNotificationHandler>();
56+
services.AddTransient<PackageRequestAcceptedNotificationHandler>();
57+
services.AddTransient<PackageRequestPendingNotificationHandler>();
5658

5759
services.AddSingleton<AuditMiddleware>();
5860

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using Altinn.AccessMgmt.Core.Services.Contracts;
4+
using Altinn.AccessMgmt.PersistenceEF.Constants;
5+
using Altinn.AccessMgmt.PersistenceEF.Extensions;
6+
using Altinn.AccessMgmt.PersistenceEF.Models;
7+
using Altinn.Authorization.Integration.Platform.Notification;
8+
using Altinn.Authorization.Integration.Platform.Notification.Models;
9+
using Altinn.Authorization.Integration.Platform.Notification.Models.Email;
10+
using Altinn.Authorization.Integration.Platform.Notification.Models.Recipient;
11+
12+
namespace Altinn.AccessMgmt.Core.Outbox;
13+
14+
public class PackageRequestAcceptedNotificationHandler(IAltinnNotification notification, IEntityService entityService) : IOutboxHandler
15+
{
16+
public async Task Handle(OutboxMessage message, CancellationToken cancellationToken)
17+
{
18+
var (recipient, sender, packages) = await GetContext(message, cancellationToken);
19+
20+
var response = await notification.Send(
21+
new()
22+
{
23+
IdempotencyId = $"auth_package_request_accept_{recipient.Id}",
24+
Recipient = await CreateRecipient(recipient, sender, packages, cancellationToken),
25+
RequestedSendTime = DateTime.UtcNow,
26+
},
27+
cancellationToken);
28+
29+
if (response.IsProblem)
30+
{
31+
throw new InvalidOperationException(response.ProblemDetails.Detail);
32+
}
33+
}
34+
35+
private async Task<(Entity Recipient, Entity Approver, IEnumerable<string> Packages)> GetContext(OutboxMessage message, CancellationToken cancellationToken)
36+
{
37+
var content = JsonSerializer.Deserialize<List<PackageRequestAcceptedNotificationMessage>>(message.Data);
38+
if (content is null || content.Count == 0)
39+
{
40+
throw new InvalidOperationException("Data is empty. Can't send notification without content.");
41+
}
42+
43+
var recipients = content.GroupBy(m => m.RecipientId);
44+
if (recipients.Count() != 1)
45+
{
46+
throw new InvalidOperationException("Outbox message contains multiple recipients, should contain only one.");
47+
}
48+
49+
var approvers = recipients.Single();
50+
var approver = content.GroupBy(m => m.AcceptorId);
51+
if (approver.Count() != 1)
52+
{
53+
throw new InvalidOperationException("Outbox message contains multiple senders, should contain only one.");
54+
}
55+
56+
var sender = approver.Single();
57+
var entityRecipient = await entityService.GetEntity(approvers.Key, cancellationToken);
58+
if (entityRecipient is null)
59+
{
60+
throw new InvalidOperationException($"Recipient entity with id '{approvers.Key}' not found.");
61+
}
62+
63+
var entitySender = await entityService.GetEntity(sender.Key, cancellationToken);
64+
if (sender is null)
65+
{
66+
throw new InvalidOperationException($"Sender entity with id '{sender.Key}' not found.");
67+
}
68+
69+
return (
70+
entityRecipient,
71+
entitySender,
72+
content.Select(m => m.Package).Distinct()
73+
);
74+
}
75+
76+
private static async Task<NotificationRecipientExt> CreateRecipient(Entity recipient, Entity approver, IEnumerable<string> packages, CancellationToken cancellationToken = default)
77+
{
78+
ArgumentNullException.ThrowIfNull(recipient);
79+
80+
if (recipient.TypeId == EntityTypeConstants.Person)
81+
{
82+
var emailContent = new StringBuilder();
83+
emailContent.AppendLine($"<p>{approver.Name} har akseptert din forespørsel om følgende tilgangspakker.</p>");
84+
emailContent.AppendLine("<ul>");
85+
foreach (var package in packages)
86+
{
87+
emailContent.AppendLine($"<li>{package}</li>");
88+
}
89+
90+
emailContent.AppendLine("</ul>");
91+
emailContent.AppendLine($"<p>Med vennnlig hilsen<b>Altinn</b></p>");
92+
93+
return new NotificationRecipientExt
94+
{
95+
RecipientPerson = new RecipientPersonExt
96+
{
97+
NationalIdentityNumber = recipient.PersonIdentifier,
98+
ChannelSchema = NotificationChannelExt.EmailAndSms,
99+
ResourceId = "altinn_access_management_hovedadmin",
100+
EmailSettings = new EmailSendingOptionsExt
101+
{
102+
Subject = "Altinn Godkjent Tilgangsforespørsel",
103+
Body = emailContent.ToString()
104+
}
105+
}
106+
};
107+
}
108+
else if (recipient.TypeId == EntityTypeConstants.Organization)
109+
{
110+
var emailContent = new StringBuilder();
111+
emailContent.AppendLine($"<p>{approver.Name} med Org.nr {recipient.OrganizationIdentifier} har akseptert din forespørsel om følgende tilgangspakker.</p>");
112+
emailContent.AppendLine("<ul>");
113+
foreach (var package in packages)
114+
{
115+
emailContent.AppendLine($"<li>{package}</li>");
116+
}
117+
118+
emailContent.AppendLine("</ul>");
119+
emailContent.AppendLine($"<p>Med vennnlig hilsen<b>Altinn</b></p>");
120+
121+
return new NotificationRecipientExt
122+
{
123+
RecipientOrganization = new RecipientOrganizationExt
124+
{
125+
OrgNumber = recipient.OrganizationIdentifier,
126+
ChannelSchema = NotificationChannelExt.EmailPreferred,
127+
ResourceId = "altinn_access_management_hovedadmin",
128+
EmailSettings = new()
129+
{
130+
Subject = "Altinn Godkjent Tilgangsforespørsel",
131+
Body = emailContent.ToString()
132+
}
133+
}
134+
};
135+
}
136+
137+
throw new InvalidOperationException("Unsupported party type. not person or organization");
138+
}
139+
}
140+
141+
/// <summary>
142+
/// Model used for deserializing content of outbox message for package request notification.
143+
/// </summary>
144+
public class PackageRequestAcceptedNotificationMessage
145+
{
146+
/// <summary>
147+
/// Entity ID of the acceptor, either person or organization.
148+
/// </summary>
149+
public Guid AcceptorId { get; set; }
150+
151+
/// <summary>
152+
/// Entity ID of the recipient, either person or organization.
153+
/// </summary>
154+
public Guid RecipientId { get; set; }
155+
156+
/// <summary>
157+
/// Name of package.
158+
/// </summary>
159+
public string Package { get; set; }
160+
161+
/// <summary>
162+
/// Used for creating a unique idempotency key and external ref id
163+
/// </summary>
164+
public DateTime RefId { get; set; }
165+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using Altinn.AccessMgmt.Core.Services.Contracts;
4+
using Altinn.AccessMgmt.PersistenceEF.Constants;
5+
using Altinn.AccessMgmt.PersistenceEF.Extensions;
6+
using Altinn.AccessMgmt.PersistenceEF.Models;
7+
using Altinn.Authorization.Integration.Platform.Notification;
8+
using Altinn.Authorization.Integration.Platform.Notification.Models;
9+
using Altinn.Authorization.Integration.Platform.Notification.Models.Email;
10+
using Altinn.Authorization.Integration.Platform.Notification.Models.Recipient;
11+
12+
namespace Altinn.AccessMgmt.Core.Outbox;
13+
14+
public class PackageRequestPendingNotificationHandler(IAltinnNotification notification, IEntityService entityService) : IOutboxHandler
15+
{
16+
public async Task Handle(OutboxMessage message, CancellationToken cancellationToken)
17+
{
18+
var (recipient, sender, packages) = await GetContext(message, cancellationToken);
19+
20+
var response = await notification.Send(
21+
new()
22+
{
23+
IdempotencyId = $"auth_package_request_accept_{recipient.Id}",
24+
Recipient = CreateRecipient(recipient, sender, packages),
25+
RequestedSendTime = DateTime.UtcNow,
26+
},
27+
cancellationToken);
28+
29+
if (response.IsProblem)
30+
{
31+
throw new InvalidOperationException(response.ProblemDetails.Detail);
32+
}
33+
}
34+
35+
private async Task<(Entity Recipient, Entity Sender, IEnumerable<string> Packages)> GetContext(OutboxMessage message, CancellationToken cancellationToken)
36+
{
37+
var content = JsonSerializer.Deserialize<List<PackageRequestPendingNotificationMessage>>(message.Data);
38+
if (content is null || content.Count == 0)
39+
{
40+
throw new InvalidOperationException("Data is empty. Can't send notification without content.");
41+
}
42+
43+
var recipients = content.GroupBy(m => m.RecipientId);
44+
if (recipients.Count() != 1)
45+
{
46+
throw new InvalidOperationException("Outbox message contains multiple recipients, should contain only one.");
47+
}
48+
49+
var recipient = recipients.Single();
50+
var senders = content.GroupBy(m => m.RequesterId);
51+
if (senders.Count() != 1)
52+
{
53+
throw new InvalidOperationException("Outbox message contains multiple senders, should contain only one.");
54+
}
55+
56+
var sender = senders.Single();
57+
var entityRecipient = await entityService.GetEntity(recipient.Key, cancellationToken);
58+
if (entityRecipient is null)
59+
{
60+
throw new InvalidOperationException($"Recipient entity with id '{recipient.Key}' not found.");
61+
}
62+
63+
var entitySender = await entityService.GetEntity(sender.Key, cancellationToken);
64+
if (sender is null)
65+
{
66+
throw new InvalidOperationException($"Sender entity with id '{sender.Key}' not found.");
67+
}
68+
69+
return (
70+
entityRecipient,
71+
entitySender,
72+
content.Select(m => m.Package).Distinct()
73+
);
74+
}
75+
76+
private static NotificationRecipientExt CreateRecipient(Entity recipient, Entity sender, IEnumerable<string> packages)
77+
{
78+
ArgumentNullException.ThrowIfNull(recipient);
79+
80+
if (recipient.TypeId == EntityTypeConstants.Person)
81+
{
82+
var emailContent = new StringBuilder();
83+
emailContent.AppendLine($"<p>{sender.Name} har bedt om følgende tilgangspakker fra deg.</p>");
84+
emailContent.AppendLine("<ul>");
85+
foreach (var package in packages)
86+
{
87+
emailContent.AppendLine($"<li>{package}</li>");
88+
}
89+
90+
emailContent.AppendLine("</ul>");
91+
emailContent.AppendLine("<p>Logg inn i Altinn, gå til tilgangsstyring og forespørsler for å behandle forespørselen.</p>");
92+
emailContent.AppendLine($"<p>Med vennnlig hilsen<b>Altinn</b></p>");
93+
94+
return new NotificationRecipientExt
95+
{
96+
RecipientPerson = new RecipientPersonExt
97+
{
98+
NationalIdentityNumber = recipient.PersonIdentifier,
99+
ChannelSchema = NotificationChannelExt.EmailAndSms,
100+
ResourceId = "altinn_access_management_hovedadmin",
101+
EmailSettings = new EmailSendingOptionsExt
102+
{
103+
Subject = "Altinn Tilgangsforespørsel",
104+
Body = emailContent.ToString()
105+
}
106+
}
107+
};
108+
}
109+
else if (recipient.TypeId == EntityTypeConstants.Organization)
110+
{
111+
var emailContent = new StringBuilder();
112+
emailContent.AppendLine($"<p>{sender.Name} har bedt om følgende tilgangspakker fra {recipient.Name} med Org.nr {recipient.OrganizationIdentifier}.</p>");
113+
emailContent.AppendLine("<ul>");
114+
foreach (var package in packages)
115+
{
116+
emailContent.AppendLine($"<li>{package}</li>");
117+
}
118+
119+
emailContent.AppendLine("</ul>");
120+
emailContent.AppendLine($"<p>Du mottar denne forespørselen fordi du har tilgangspakken hovedaministrator for {recipient.Name} i Altinn. Logg inn i Altinn velg riktig aktør og gå til tilgangsstyring og forespørsler for å behandle forespørselen.</p>");
121+
emailContent.AppendLine($"<p>Med vennnlig hilsen<b>Altinn</b></p>");
122+
123+
return new NotificationRecipientExt
124+
{
125+
RecipientOrganization = new RecipientOrganizationExt
126+
{
127+
OrgNumber = recipient.OrganizationIdentifier,
128+
ChannelSchema = NotificationChannelExt.EmailPreferred,
129+
ResourceId = "altinn_access_management_hovedadmin",
130+
EmailSettings = new()
131+
{
132+
Subject = "Altinn Tilgangsforespørsel",
133+
Body = emailContent.ToString()
134+
}
135+
}
136+
};
137+
}
138+
139+
throw new InvalidOperationException("Unsupported party type. not person or organization");
140+
}
141+
}
142+
143+
/// <summary>
144+
/// Model used for deserializing content of outbox message for package request notification.
145+
/// </summary>
146+
public class PackageRequestPendingNotificationMessage
147+
{
148+
/// <summary>
149+
/// Entity ID of the requester, either person or organization.
150+
/// </summary>
151+
public Guid RequesterId { get; set; }
152+
153+
/// <summary>
154+
/// Entity ID of the recipient, either person or organization.
155+
/// </summary>
156+
public Guid RecipientId { get; set; }
157+
158+
/// <summary>
159+
/// Name of package.
160+
/// </summary>
161+
public string Package { get; set; }
162+
163+
/// <summary>
164+
/// Used for creating a unique idempotency key and external ref id
165+
/// </summary>
166+
public DateTime RefId { get; set; }
167+
}

0 commit comments

Comments
 (0)