From 4bf00ac9305ac8ee09f25489c7825f0cfa259030 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Fri, 3 May 2024 13:11:40 +0200 Subject: [PATCH 01/36] Add authorization service --- .../v0.29/01-functions-and-procedures.sql | 393 +++++++++++++++++- .../Authorization/AuthorizationService.cs | 159 +++++++ .../Authorization/IAuthorizationService.cs | 20 + .../Controllers/TestController.cs | 49 +++ src/Altinn.Notifications/Program.cs | 8 + src/Altinn.Notifications/appsettings.json | 4 +- 6 files changed, 631 insertions(+), 2 deletions(-) create mode 100644 src/Altinn.Notifications/Authorization/AuthorizationService.cs create mode 100644 src/Altinn.Notifications/Authorization/IAuthorizationService.cs create mode 100644 src/Altinn.Notifications/Controllers/TestController.cs diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql index 5f088eea..768b371f 100644 --- a/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql +++ b/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql @@ -1 +1,392 @@ --- This script is autogenerated from the tool DbTools. Do not edit manually. \ No newline at end of file +-- This script is autogenerated from the tool DbTools. Do not edit manually. + +-- getemailrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + toaddress text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _alternateid); +BEGIN +RETURN query + SELECT e.recipientorgno, e.recipientnin, e.toaddress + FROM notifications.emailnotifications e + WHERE e._orderid = __orderid; +END; +$BODY$; + +-- getemailsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + latest_email_timeout TIMESTAMP WITH TIME ZONE; +BEGIN + SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN query + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress) + SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype + FROM updated u, notifications.emailtexts et + WHERE u._orderid = et._orderid; +END; +$BODY$; + +-- getemailsummary.sql: +CREATE OR REPLACE FUNCTION notifications.getemailsummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + toaddress text, + result emailnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.toaddress, n.result, n.resulttime + FROM notifications.emailnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- getmetrics.sql: +CREATE OR REPLACE FUNCTION notifications.getmetrics( + month_input int, + year_input int +) +RETURNS TABLE ( + org text, + placed_orders bigint, + sent_emails bigint, + succeeded_emails bigint, + sent_sms bigint, + succeeded_sms bigint +) +AS $$ +BEGIN + RETURN QUERY + SELECT + o.creatorname, + COUNT(DISTINCT o._id) AS placed_orders, + SUM(CASE WHEN e._id IS NOT NULL THEN 1 ELSE 0 END) AS sent_emails, + SUM(CASE WHEN e.result = 'Succeeded' THEN 1 ELSE 0 END) AS succeeded_emails, + SUM(CASE WHEN s._id IS NOT NULL THEN s.smscount ELSE 0 END) AS sent_sms, + SUM(CASE WHEN s.result = 'Accepted' THEN 1 ELSE 0 END) AS succeeded_sms + FROM notifications.orders o + LEFT JOIN notifications.emailnotifications e ON o._id = e._orderid + LEFT JOIN notifications.smsnotifications s ON o._id = s._orderid + WHERE EXTRACT(MONTH FROM o.requestedsendtime) = month_input + AND EXTRACT(YEAR FROM o.requestedsendtime) = year_input + GROUP BY o.creatorname; +END; +$$ LANGUAGE plpgsql; + + +-- getorderincludestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorder_includestatus_v2( + _alternateid uuid, + _creatorname text +) +RETURNS TABLE( + alternateid uuid, + creatorname text, + sendersreference text, + created timestamp with time zone, + requestedsendtime timestamp with time zone, + processed timestamp with time zone, + processedstatus orderprocessingstate, + notificationchannel text, + generatedemailcount bigint, + succeededemailcount bigint, + generatedsmscount bigint, + succeededsmscount bigint +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + _target_orderid INTEGER; + _succeededEmailCount BIGINT; + _generatedEmailCount BIGINT; + _succeededSmsCount BIGINT; + _generatedSmsCount BIGINT; +BEGIN + SELECT _id INTO _target_orderid + FROM notifications.orders + WHERE orders.alternateid = _alternateid + AND orders.creatorname = _creatorname; + + SELECT + SUM(CASE WHEN result = 'Succeeded' THEN 1 ELSE 0 END), + COUNT(1) AS generatedEmailCount + INTO _succeededEmailCount, _generatedEmailCount + FROM notifications.emailnotifications + WHERE _orderid = _target_orderid; + + SELECT + SUM(CASE WHEN result = 'Accepted' THEN 1 ELSE 0 END), + COUNT(1) AS generatedSmsCount + INTO _succeededSmsCount, _generatedSmsCount + FROM notifications.smsnotifications + WHERE _orderid = _target_orderid; + + RETURN QUERY + SELECT + orders.alternateid, + orders.creatorname, + orders.sendersreference, + orders.created, + orders.requestedsendtime, + orders.processed, + orders.processedstatus, + orders.notificationorder->>'NotificationChannel', + _generatedEmailCount, + _succeededEmailCount, + _generatedSmsCount, + _succeededSmsCount + FROM + notifications.orders AS orders + WHERE + orders.alternateid = _alternateid; +END; +$BODY$; + + +-- getorderspastsendtimeupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorders_pastsendtime_updatestatus() + RETURNS TABLE(notificationorders jsonb) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +RETURN QUERY + UPDATE notifications.orders + SET processedstatus = 'Processing' + WHERE _id IN (select _id + from notifications.orders + where processedstatus = 'Registered' + and requestedsendtime <= now() + INTERVAL '1 minute' + limit 50) + RETURNING notificationorder AS notificationorders; +END; +$BODY$; + +-- getsmsrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getsmsrecipients_v2(_orderid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + mobilenumber text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN +RETURN query + SELECT s.recipientorgno, s.recipientnin, s.mobilenumber + FROM notifications.smsnotifications s + WHERE s._orderid = __orderid; +END; +$BODY$; + +-- getsmsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + + RETURN query + WITH updated AS ( + UPDATE notifications.smsnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber) + SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body + FROM updated u, notifications.smstexts st + WHERE u._orderid = st._orderid; +END; +$BODY$; + +-- getsmssummary.sql: +CREATE OR REPLACE FUNCTION notifications.getsmssummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + mobilenumber text, + result smsnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.mobilenumber, n.result, n.resulttime + FROM notifications.smsnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::smsnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- insertemailnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_toaddress TEXT, +_result text, +_resulttime timestamptz, +_expirytime timestamptz) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN + +INSERT INTO notifications.emailnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +toaddress, result, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_toaddress, +_result::emailnotificationresulttype, +_resulttime, +_expirytime); +END; +$BODY$; + +-- insertemailtext.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailtext(__orderid BIGINT, _fromaddress TEXT, _subject TEXT, _body TEXT, _contenttype TEXT) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +INSERT INTO notifications.emailtexts(_orderid, fromaddress, subject, body, contenttype) + VALUES (__orderid, _fromaddress, _subject, _body, _contenttype); +END; +$BODY$; + + +-- insertorder.sql: +CREATE OR REPLACE FUNCTION notifications.insertorder(_alternateid UUID, _creatorname TEXT, _sendersreference TEXT, _created TIMESTAMPTZ, _requestedsendtime TIMESTAMPTZ, _notificationorder JSONB) +RETURNS BIGINT + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +_orderid BIGINT; +BEGIN + INSERT INTO notifications.orders(alternateid, creatorname, sendersreference, created, requestedsendtime, processed, notificationorder) + VALUES (_alternateid, _creatorname, _sendersreference, _created, _requestedsendtime, _created, _notificationorder) + RETURNING _id INTO _orderid; + + RETURN _orderid; +END; +$BODY$; + +-- insertsmsnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_mobilenumber TEXT, +_result text, +_smscount integer, +_resulttime timestamptz, +_expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN + +INSERT INTO notifications.smsnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +mobilenumber, +result, +smscount, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_mobilenumber, +_result::smsnotificationresulttype, +_smscount, +_resulttime, +_expirytime); +END; +$BODY$; + +-- updateemailstatus.sql: +CREATE OR REPLACE PROCEDURE notifications.updateemailstatus(_alternateid UUID, _result text, _operationid text) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + UPDATE notifications.emailnotifications + SET result = _result::emailnotificationresulttype, resulttime = now(), operationid = _operationid + WHERE alternateid = _alternateid; +END; +$BODY$; + diff --git a/src/Altinn.Notifications/Authorization/AuthorizationService.cs b/src/Altinn.Notifications/Authorization/AuthorizationService.cs new file mode 100644 index 00000000..53eab63c --- /dev/null +++ b/src/Altinn.Notifications/Authorization/AuthorizationService.cs @@ -0,0 +1,159 @@ +using System.Security.Claims; +using System.Text.Json; + +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Constants; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Interfaces; + +using static Altinn.Authorization.ABAC.Constants.XacmlConstants; + +namespace Altinn.Notifications.Authorization; + +/// +/// An implementation of able to check that a potential +/// recipient of a notification can access the resource that the notification is about. +/// +public class AuthorizationService : IAuthorizationService +{ + private const string UserIdUrn = "urn:altinn:userid"; + + private const string DefaultIssuer = "Altinn"; + private const string ActionCategoryId = "action"; + private const string ResourceCategoryId = "resource"; + private const string AccessSubjectCategoryIdPrefix = "subject"; + + private readonly IPDP _pdp; + + /// + /// Initialize a new instance the class with the given dependenices. + /// + public AuthorizationService(IPDP pdp) + { + _pdp = pdp; + } + + /// + /// An implementation of that + /// will generate an authorization call to Altinn Authorization to check that the given users have read access. + /// + /// The list of user ids. + /// The id of the resource. + /// The party id of the resource owner. + /// A task + public async Task> AuthorizeUsersForResource(List userIds, string resourceId, int resourceOwnerId) + { + XacmlJsonRequest request = new() + { + AccessSubject = [], + Action = [CreateActionCategory()], + Resource = [CreateResourceCategory(resourceId, resourceOwnerId)], + MultiRequests = new XacmlJsonMultiRequests { RequestReference = [] } + }; + + foreach (int userId in userIds.Distinct()) + { + XacmlJsonCategory subjectCategory = CreateAccessSubjectCategory(userId); + request.AccessSubject.Add(subjectCategory); + request.MultiRequests.RequestReference.Add(CreateRequestReference(subjectCategory.Id)); + } + + XacmlJsonRequestRoot jsonRequest = new() { Request = request }; + + XacmlJsonResponse xacmlJsonResponse = await _pdp.GetDecisionForRequest(jsonRequest); + + Dictionary keyValuePairs = []; + + foreach (var response in xacmlJsonResponse.Response) + { + XacmlJsonCategory? xacmlJsonCategory = response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Subject); + + if (xacmlJsonCategory is not null) + { + XacmlJsonAttribute? xacmlJsonAttribute = xacmlJsonCategory.Attribute.Find(a => a.AttributeId == UserIdUrn); + + if (xacmlJsonAttribute is not null) + { + keyValuePairs.Add(xacmlJsonAttribute.Value, true); + } + } + } + + return keyValuePairs; + } + + private XacmlJsonCategory CreateActionCategory() + { + XacmlJsonAttribute attribute = + DecisionHelper.CreateXacmlJsonAttribute( + MatchAttributeIdentifiers.ActionId, "read", "string", DefaultIssuer); + + return new XacmlJsonCategory() + { + Id = ActionCategoryId, + Attribute = [attribute] + }; + } + + private static XacmlJsonCategory CreateResourceCategory(string resourceId, int resourceOwnerId) + { + XacmlJsonAttribute subjectAttribute = + DecisionHelper.CreateXacmlJsonAttribute( + AltinnXacmlUrns.PartyId, resourceOwnerId.ToString(), ClaimValueTypes.String, DefaultIssuer); + + if (resourceId.StartsWith("app_")) + { + string[] appResource = resourceId.Split('_'); + + XacmlJsonAttribute orgAttribute = + DecisionHelper.CreateXacmlJsonAttribute( + AltinnXacmlUrns.OrgId, appResource[1], ClaimValueTypes.String, DefaultIssuer); + + XacmlJsonAttribute appAttribute = + DecisionHelper.CreateXacmlJsonAttribute( + AltinnXacmlUrns.AppId, appResource[2], ClaimValueTypes.String, DefaultIssuer); + + return new XacmlJsonCategory() + { + Id = ResourceCategoryId, + Attribute = [orgAttribute, appAttribute, subjectAttribute] + }; + } + + XacmlJsonAttribute resourceAttribute = + DecisionHelper.CreateXacmlJsonAttribute( + AltinnXacmlUrns.ResourceId, resourceId, ClaimValueTypes.String, DefaultIssuer); + + return new XacmlJsonCategory() + { + Id = ResourceCategoryId, + Attribute = [resourceAttribute, subjectAttribute] + }; + } + + private XacmlJsonCategory CreateAccessSubjectCategory(int userId) + { + XacmlJsonAttribute attribute = + DecisionHelper.CreateXacmlJsonAttribute( + UserIdUrn, userId.ToString(), ClaimValueTypes.String, DefaultIssuer, true); + + return new XacmlJsonCategory() + { + Id = AccessSubjectCategoryIdPrefix + userId, + Attribute = [attribute] + }; + } + + private static XacmlJsonRequestReference CreateRequestReference(string subjectCategoryId) + { + return new XacmlJsonRequestReference + { + ReferenceId = new List + { + subjectCategoryId, + ActionCategoryId, + ResourceCategoryId + } + }; + } +} diff --git a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs b/src/Altinn.Notifications/Authorization/IAuthorizationService.cs new file mode 100644 index 00000000..0796da7a --- /dev/null +++ b/src/Altinn.Notifications/Authorization/IAuthorizationService.cs @@ -0,0 +1,20 @@ +using Altinn.Authorization.ABAC.Xacml.JsonProfile; + +namespace Altinn.Notifications.Authorization; + +/// +/// Describes the necessary functions of an authorization service that can perform +/// notification recipient filtering based on authorization +/// +public interface IAuthorizationService +{ + /// + /// Describes a method that can create an authorization request to authorize a set of + /// users for access to a resource. + /// + /// The list of user ids. + /// The id of the resource. + /// The party id of the resource owner. + /// A task + Task> AuthorizeUsersForResource(List userIds, string resourceId, int resourceOwnerId); +} diff --git a/src/Altinn.Notifications/Controllers/TestController.cs b/src/Altinn.Notifications/Controllers/TestController.cs new file mode 100644 index 00000000..3e378dd0 --- /dev/null +++ b/src/Altinn.Notifications/Controllers/TestController.cs @@ -0,0 +1,49 @@ +using Altinn.Authorization.ABAC.Xacml.JsonProfile; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Notifications.Controllers +{ + /// + /// Temporary adding a controller in order to generate som test data from Authorization. + /// + [ApiController] + [Route("notifications/api/v1/test")] + [AllowAnonymous] + public class TestController(Authorization.IAuthorizationService authorizationService) + : ControllerBase + { + private readonly Authorization.IAuthorizationService _authorizationService = authorizationService; + + /// + /// Test method for authorization. + /// + [HttpPost] + public async Task> Authorize([FromBody]AuthZ authz) + { + return await _authorizationService.AuthorizeUsersForResource(authz.UserIds, authz.ResourceId, authz.ResourceOwnerId); + } + } + + /// + /// Request input + /// + public class AuthZ + { + /// + /// List of users + /// + public List UserIds { get; set; } = []; + + /// + /// Resource + /// + public string ResourceId { get; set; } = string.Empty; + + /// + /// Resource + /// + public int ResourceOwnerId { get; set; } + } +} diff --git a/src/Altinn.Notifications/Program.cs b/src/Altinn.Notifications/Program.cs index dc3f9b6c..cdd8965a 100644 --- a/src/Altinn.Notifications/Program.cs +++ b/src/Altinn.Notifications/Program.cs @@ -6,6 +6,9 @@ using Altinn.Common.AccessToken; using Altinn.Common.AccessToken.Services; using Altinn.Common.PEP.Authorization; +using Altinn.Common.PEP.Clients; +using Altinn.Common.PEP.Implementation; +using Altinn.Common.PEP.Interfaces; using Altinn.Notifications.Authorization; using Altinn.Notifications.Configuration; using Altinn.Notifications.Core.Extensions; @@ -182,6 +185,11 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) AddInputModelValidators(services); services.AddCoreServices(config); + services.Configure(config.GetSection("PlatformSettings")); + services.AddHttpClient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddKafkaServices(config); services.AddAltinnClients(config); services.AddPostgresRepositories(config); diff --git a/src/Altinn.Notifications/appsettings.json b/src/Altinn.Notifications/appsettings.json index 86120c75..cca5c97f 100644 --- a/src/Altinn.Notifications/appsettings.json +++ b/src/Altinn.Notifications/appsettings.json @@ -1,6 +1,8 @@ { "PlatformSettings": { - "ApiProfileEndpoint": "http://localhost:5030/profile/api/v1/" + "ApiProfileEndpoint": "http://localhost:5030/profile/api/v1/", + "ApiAuthorizationEndpoint": "https://platform.at24.altinn.cloud/authorization/api/v1/", + "SubscriptionKey": "2ab0b678b6e94436a5b05f61df159676" }, "PostgreSQLSettings": { "MigrationScriptPath": "Migration", From 6988b98aa77f0ebc85dce81a9ad19cc54321cf06 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Tue, 7 May 2024 17:07:32 +0200 Subject: [PATCH 02/36] current progress --- .../Helpers/MobileNumberHelper.cs | 7 +- .../Integrations/IProfileClient.cs | 8 + .../OrganizationContactPoints.cs | 10 + .../Models/Orders/IBaseNotificationOrder.cs | 9 +- .../Models/Orders/NotificationOrder.cs | 7 +- .../Models/Orders/NotificationOrderRequest.cs | 15 +- .../Orders/NotificationOrderWithStatus.cs | 3 + .../Services/ContactPointService.cs | 64 ++- .../Services/EmailOrderProcessingService.cs | 4 +- .../Interfaces/IContactPointService.cs | 6 +- .../Services/OrderRequestService.cs | 11 +- .../Services/SmsOrderProcessingService.cs | 4 +- .../Profile/OrganizationContactPointsList.cs | 15 + .../Profile/ProfileClient.cs | 24 ++ .../Profile/UnitContactPointLookup.cs | 18 + .../v0.29/01-functions-and-procedures.sql | 393 +++++++++++++++++- .../EmailNotificationOrdersController.cs | 6 +- .../Mappers/OrderMapper.cs | 9 +- .../EmailNotificationOrderRequestExt.cs | 6 + .../Models/IBaseNotificationOrderExt.cs | 16 +- .../Models/NotificationOrderExt.cs | 8 +- .../Models/NotificationOrderWithStatusExt.cs | 8 + .../Models/SmsNotificationOrderRequestExt.cs | 6 + .../OrdersController/OrdersControllerTests.cs | 3 +- .../ContactPointServiceTests.cs | 8 +- .../EmailOrderProcessingServiceTests.cs | 6 +- .../OrderRequestServiceTests.cs | 4 +- .../SmsOrderProcessingServiceTests.cs | 6 +- 28 files changed, 634 insertions(+), 50 deletions(-) create mode 100644 src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs create mode 100644 src/Altinn.Notifications.Integrations/Profile/UnitContactPointLookup.cs diff --git a/src/Altinn.Notifications.Core/Helpers/MobileNumberHelper.cs b/src/Altinn.Notifications.Core/Helpers/MobileNumberHelper.cs index 3e4f5fad..ea01a834 100644 --- a/src/Altinn.Notifications.Core/Helpers/MobileNumberHelper.cs +++ b/src/Altinn.Notifications.Core/Helpers/MobileNumberHelper.cs @@ -15,7 +15,12 @@ public static class MobileNumberHelper /// public static string EnsureCountryCodeIfValidNumber(string mobileNumber) { - if (mobileNumber.StartsWith("00")) + if (string.IsNullOrEmpty(mobileNumber)) + { + return mobileNumber; + } + else if + (mobileNumber.StartsWith("00")) { mobileNumber = "+" + mobileNumber.Remove(0, 2); } diff --git a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs index 4996c5d5..28ed22d5 100644 --- a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs @@ -13,4 +13,12 @@ public interface IProfileClient /// A list of national identity numbers to look up contact points for /// A list of contact points for the provided national identity numbers public Task> GetUserContactPoints(List nationalIdentityNumbers); + + /// + /// Retrieves the user registered contact points for a list of organization corresponding to a list of organization numbers + /// + /// The id of the resource to look up contact points for + /// The set or organizations to retrieve contact points for + /// + public Task> GetUserRegisteredOrganizationContactPoints(string resourceId, List organizationNumbers); } diff --git a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs index a95d8cf3..45ebde36 100644 --- a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs +++ b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs @@ -10,6 +10,11 @@ public class OrganizationContactPoints /// public string OrganizationNumber { get; set; } = string.Empty; + /// + /// Gets or sets the party id of the organization + /// + public int PartyId { get; set; } + /// /// Gets or sets a list of official mobile numbers /// @@ -19,4 +24,9 @@ public class OrganizationContactPoints /// Gets or sets a list of official email addresses /// public List EmailList { get; set; } = []; + + /// + /// Gets or sets a list of user registered contanct points associated with the organisation. + /// + public List UserContactPoints { get; set; } = []; } diff --git a/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs b/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs index 47ead127..98f728dc 100644 --- a/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs +++ b/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs @@ -1,4 +1,6 @@ -using Altinn.Notifications.Core.Enums; +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; namespace Altinn.Notifications.Core.Models.Orders; @@ -32,6 +34,11 @@ public interface IBaseNotificationOrder /// public bool IgnoreReservation { get; } + /// + /// Gets or sets the id of the resource that the notification is related to + /// + public string? ResourceId { get; } + /// /// Gets the creator of the notification /// diff --git a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs index e0058aad..255120f4 100644 --- a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs +++ b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrder.cs @@ -25,6 +25,9 @@ public class NotificationOrder : IBaseNotificationOrder /// > public bool IgnoreReservation { get; internal set; } + /// > + public string? ResourceId { get; internal set; } + /// > public Creator Creator { get; internal set; } @@ -53,7 +56,8 @@ public NotificationOrder( Creator creator, DateTime created, List recipients, - bool ignoreReservation) + bool ignoreReservation, + string? resourceId) { Id = id; SendersReference = sendersReference; @@ -64,6 +68,7 @@ public NotificationOrder( Created = created; Recipients = recipients; IgnoreReservation = ignoreReservation; + ResourceId = resourceId; } /// diff --git a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs index 339f1be2..fa7af4d1 100644 --- a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs +++ b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs @@ -1,4 +1,6 @@ -using Altinn.Notifications.Core.Enums; +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models.NotificationTemplate; namespace Altinn.Notifications.Core.Models.Orders; @@ -39,10 +41,15 @@ public class NotificationOrderRequest public Creator Creator { get; internal set; } /// - /// Gets or sets whether notifications generated by this order should ignore KRR reservations + /// Gets a boolean indicating whether notifications generated by this order should ignore KRR reservations /// public bool IgnoreReservation { get; internal set; } + /// + /// Gets the id of the resource that the notification is related to + /// + public string? ResourceId { get; internal set; } + /// /// Initializes a new instance of the class. /// @@ -53,7 +60,8 @@ public NotificationOrderRequest( DateTime requestedSendTime, NotificationChannel notificationChannel, List recipients, - bool ignoreReservation = false) + bool ignoreReservation = false, + string? resourceId = null) { SendersReference = sendersReference; Creator = new(creatorShortName); @@ -62,6 +70,7 @@ public NotificationOrderRequest( NotificationChannel = notificationChannel; Recipients = recipients; IgnoreReservation = ignoreReservation; + ResourceId = resourceId; } /// diff --git a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderWithStatus.cs b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderWithStatus.cs index ea6ea2e6..42dc3abd 100644 --- a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderWithStatus.cs +++ b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderWithStatus.cs @@ -30,6 +30,9 @@ public class NotificationOrderWithStatus : IBaseNotificationOrder /// > public bool IgnoreReservation { get; internal set; } + /// > + public string? ResourceId { get; internal set; } + /// /// Gets the processing status of the notication order /// diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index fe170001..cd963857 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -25,10 +25,11 @@ public ContactPointService(IProfileClient profile, IRegisterClient register) } /// - public async Task AddEmailContactPoints(List recipients) + public async Task AddEmailContactPoints(List recipients, string? resourceId) { await AugmentRecipients( recipients, + resourceId, (recipient, userContactPoints) => { if (!string.IsNullOrEmpty(userContactPoints.Email)) @@ -40,16 +41,24 @@ await AugmentRecipients( }, (recipient, orgContactPoints) => { - recipient.AddressInfo.AddRange(orgContactPoints.EmailList.Select(e => new EmailAddressPoint(e)).ToList()); + recipient.AddressInfo.AddRange(orgContactPoints.EmailList + .Select(e => new EmailAddressPoint(e)) + .ToList()); + + recipient.AddressInfo.AddRange(orgContactPoints.UserContactPoints + .Where(u => !string.IsNullOrEmpty(u.Email)) + .Select(u => new EmailAddressPoint(u.Email)) + .ToList()); return recipient; }); } /// - public async Task AddSmsContactPoints(List recipients) + public async Task AddSmsContactPoints(List recipients, string? resourceId) { await AugmentRecipients( recipients, + resourceId, (recipient, userContactPoints) => { if (!string.IsNullOrEmpty(userContactPoints.MobileNumber)) @@ -61,20 +70,28 @@ await AugmentRecipients( }, (recipient, orgContactPoints) => { - recipient.AddressInfo.AddRange(orgContactPoints.MobileNumberList.Select(m => new SmsAddressPoint(m)).ToList()); + recipient.AddressInfo.AddRange(orgContactPoints.MobileNumberList + .Select(m => new SmsAddressPoint(m)) + .ToList()); + + recipient.AddressInfo.AddRange(orgContactPoints.UserContactPoints + .Where(u => !string.IsNullOrEmpty(u.MobileNumber)) + .Select(u => new SmsAddressPoint(u.MobileNumber)) + .ToList()); return recipient; }); } private async Task> AugmentRecipients( List recipients, + string? resourceId, Func createUserContactPoint, Func createOrgContactPoint) { List augmentedRecipients = []; var userLookupTask = LookupPersonContactPoints(recipients); - var orgLookupTask = LookupOrganizationContactPoints(recipients); + var orgLookupTask = LookupOrganizationContactPoints(recipients, resourceId); await Task.WhenAll(userLookupTask, orgLookupTask); List userContactPointsList = userLookupTask.Result; @@ -130,10 +147,8 @@ private async Task> LookupPersonContactPoints(List> LookupOrganizationContactPoints(List recipients) + private async Task> LookupOrganizationContactPoints(List recipients, string? resourceId = null) { - /* the output from this function should include an AUHTORIZED list of user registered contact points if notification has a service affiliation - will require the extension of the OrganizationContactPoints class */ List orgNos = recipients .Where(r => !string.IsNullOrEmpty(r.OrganizationNumber)) .Select(r => r.OrganizationNumber!) @@ -144,7 +159,38 @@ will require the extension of the OrganizationContactPoints class */ return []; } - List contactPoints = await _registerClient.GetOrganizationContactPoints(orgNos); + Task> registerTask = _registerClient.GetOrganizationContactPoints(orgNos); + List userRegisteredContactPoints = new(); + + if (!string.IsNullOrEmpty(resourceId)) + { + // TODO: call authorization to filter list before moving forward + userRegisteredContactPoints = await _profileClient.GetUserRegisteredOrganizationContactPoints(resourceId, orgNos); + } + + List contactPoints = await registerTask; + + if (!string.IsNullOrEmpty(resourceId)) + { + foreach (var userContactPoint in userRegisteredContactPoints) + { + userContactPoint.UserContactPoints.ForEach(userContactPoint => + { + userContactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(userContactPoint.MobileNumber); + }); + + var existingContactPoint = contactPoints.FirstOrDefault(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); + + if (existingContactPoint != null) + { + existingContactPoint.UserContactPoints.AddRange(userContactPoint.UserContactPoints); + } + else + { + contactPoints.Add(userContactPoint); + } + } + } contactPoints.ForEach(contactPoint => { diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs index 1bf2b64a..5888fce2 100644 --- a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -36,7 +36,7 @@ public async Task ProcessOrder(NotificationOrder order) var recipients = order.Recipients; var recipientsWithoutEmail = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)).ToList(); - await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail); + await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); foreach (Recipient recipient in recipients) { @@ -50,7 +50,7 @@ public async Task ProcessOrderRetry(NotificationOrder order) var recipients = order.Recipients; var recipientsWithoutEmail = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Email)).ToList(); - await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail); + await _contactPointService.AddEmailContactPoints(recipientsWithoutEmail, order.ResourceId); List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs index cc4cea26..48f21384 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IContactPointService.cs @@ -11,16 +11,18 @@ public interface IContactPointService /// Looks up and adds the email contact points for recipients based on their national identity number or organization number /// /// List of recipients to retrieve contact points for + /// The resource to find contact points in relation to /// The list of recipients augumented with email address points where available /// Implementation alters the recipient reference object directly - public Task AddEmailContactPoints(List recipients); + public Task AddEmailContactPoints(List recipients, string? resourceId); /// /// Looks up and adds the SMS contact points for recipients based on their national identity number or organization number /// /// List of recipients to retrieve contact points for + /// The resource to find contact points in relation to /// The list of recipients augumented with SMS address points where available /// Implementation alters the recipient reference object directly - public Task AddSmsContactPoints(List recipients); + public Task AddSmsContactPoints(List recipients, string? resourceId); } } diff --git a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs index 552ee4b1..ba0b79e2 100644 --- a/src/Altinn.Notifications.Core/Services/OrderRequestService.cs +++ b/src/Altinn.Notifications.Core/Services/OrderRequestService.cs @@ -49,7 +49,7 @@ public async Task RegisterNotificationOrder(No // copying recipients by value to not alter the orderRequest that will be persisted var copiedRecipents = orderRequest.Recipients.Select(r => r.DeepCopy()).ToList(); - var lookupResult = await GetRecipientLookupResult(copiedRecipents, orderRequest.NotificationChannel); + var lookupResult = await GetRecipientLookupResult(copiedRecipents, orderRequest.NotificationChannel, orderRequest.ResourceId); var templates = SetSenderIfNotDefined(orderRequest.Templates); @@ -62,7 +62,8 @@ public async Task RegisterNotificationOrder(No orderRequest.Creator, created, orderRequest.Recipients, - orderRequest.IgnoreReservation); + orderRequest.IgnoreReservation, + orderRequest.ResourceId); NotificationOrder savedOrder = await _repository.Create(order); @@ -73,7 +74,7 @@ public async Task RegisterNotificationOrder(No }; } - private async Task GetRecipientLookupResult(List recipients, NotificationChannel channel) + private async Task GetRecipientLookupResult(List recipients, NotificationChannel channel, string? resourceId) { List recipientsWithoutContactPoint = []; @@ -96,11 +97,11 @@ public async Task RegisterNotificationOrder(No if (channel == NotificationChannel.Email) { - await _contactPointService.AddEmailContactPoints(recipientsWithoutContactPoint); + await _contactPointService.AddEmailContactPoints(recipientsWithoutContactPoint, resourceId); } else if (channel == NotificationChannel.Sms) { - await _contactPointService.AddSmsContactPoints(recipientsWithoutContactPoint); + await _contactPointService.AddSmsContactPoints(recipientsWithoutContactPoint, resourceId); } var isReserved = recipients.Where(r => r.IsReserved.HasValue && r.IsReserved.Value).Select(r => r.NationalIdentityNumber!).ToList(); diff --git a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs index 73e0fe5c..a8ffc527 100644 --- a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -36,7 +36,7 @@ public async Task ProcessOrder(NotificationOrder order) { var recipients = order.Recipients; var recipientsWithoutMobileNumber = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)).ToList(); - await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber); + await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); int smsCount = GetSmsCountForOrder(order); @@ -52,7 +52,7 @@ public async Task ProcessOrderRetry(NotificationOrder order) var recipients = order.Recipients; var recipientsWithoutMobileNumber = recipients.Where(r => !r.AddressInfo.Exists(ap => ap.AddressType == AddressType.Sms)).ToList(); - await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber); + await _contactPointService.AddSmsContactPoints(recipientsWithoutMobileNumber, order.ResourceId); int smsCount = GetSmsCountForOrder(order); List smsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); diff --git a/src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs b/src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs new file mode 100644 index 00000000..ca6108e1 --- /dev/null +++ b/src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs @@ -0,0 +1,15 @@ +using Altinn.Notifications.Core.Models.ContactPoints; + +namespace Altinn.Notifications.Integrations.Profile +{ + /// + /// A list representation of + /// + public class OrganizationContactPointsList + { + /// + /// A list containing contact points for users + /// + public List ContactPointsList { get; set; } = []; + } +} diff --git a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs index 35ba2788..9a55ad28 100644 --- a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs +++ b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs @@ -49,4 +49,28 @@ public async Task> GetUserContactPoints(List nat List? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; return contactPoints!; } + + /// + public async Task> GetUserRegisteredOrganizationContactPoints(string resourceId, List organizationNumbers) + { + var lookupObject = new UnitContactPointLookup() + { + ResourceId = resourceId, + OrganizationNumbers = organizationNumbers + }; + + HttpContent content = new StringContent(JsonSerializer.Serialize(lookupObject, JsonSerializerOptionsProvider.Options), Encoding.UTF8, "application/json"); + + var response = await _client.PostAsync("units/contactpoint/lookup", content); + + if (!response.IsSuccessStatusCode) + { + throw new PlatformHttpException(response, $"ProfileClient.GetUserRegisteredOrganizationContactPoints failed with status code {response.StatusCode}"); + } + + string responseContent = await response.Content.ReadAsStringAsync(); + List? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; + + return contactPoints!; + } } diff --git a/src/Altinn.Notifications.Integrations/Profile/UnitContactPointLookup.cs b/src/Altinn.Notifications.Integrations/Profile/UnitContactPointLookup.cs new file mode 100644 index 00000000..d20568d7 --- /dev/null +++ b/src/Altinn.Notifications.Integrations/Profile/UnitContactPointLookup.cs @@ -0,0 +1,18 @@ +namespace Altinn.Notifications.Integrations.Profile +{ + /// + /// A class describing the query model for contact points for units + /// + public class UnitContactPointLookup + { + /// + /// Gets or sets the list of organisation numbers to lookup contact points for + /// + public List OrganizationNumbers { get; set; } = []; + + /// + /// Gets or sets the resource id to filter the contact points by + /// + public string ResourceId { get; set; } = string.Empty; + } +} diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql b/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql index 5f088eea..768b371f 100644 --- a/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql +++ b/src/Altinn.Notifications.Persistence/Migration/v0.29/01-functions-and-procedures.sql @@ -1 +1,392 @@ --- This script is autogenerated from the tool DbTools. Do not edit manually. \ No newline at end of file +-- This script is autogenerated from the tool DbTools. Do not edit manually. + +-- getemailrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getemailrecipients_v2(_alternateid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + toaddress text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _alternateid); +BEGIN +RETURN query + SELECT e.recipientorgno, e.recipientnin, e.toaddress + FROM notifications.emailnotifications e + WHERE e._orderid = __orderid; +END; +$BODY$; + +-- getemailsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getemails_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, subject text, body text, fromaddress text, toaddress text, contenttype text) + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + latest_email_timeout TIMESTAMP WITH TIME ZONE; +BEGIN + SELECT emaillimittimeout INTO latest_email_timeout FROM notifications.resourcelimitlog WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + IF latest_email_timeout IS NOT NULL THEN + IF latest_email_timeout >= NOW() THEN + RETURN QUERY SELECT NULL::uuid AS alternateid, NULL::text AS subject, NULL::text AS body, NULL::text AS fromaddress, NULL::text AS toaddress, NULL::text AS contenttype WHERE FALSE; + RETURN; + ELSE + UPDATE notifications.resourcelimitlog SET emaillimittimeout = NULL WHERE id = (SELECT MAX(id) FROM notifications.resourcelimitlog); + END IF; + END IF; + + RETURN query + WITH updated AS ( + UPDATE notifications.emailnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.emailnotifications.alternateid, _orderid, notifications.emailnotifications.toaddress) + SELECT u.alternateid, et.subject, et.body, et.fromaddress, u.toaddress, et.contenttype + FROM updated u, notifications.emailtexts et + WHERE u._orderid = et._orderid; +END; +$BODY$; + +-- getemailsummary.sql: +CREATE OR REPLACE FUNCTION notifications.getemailsummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + toaddress text, + result emailnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.toaddress, n.result, n.resulttime + FROM notifications.emailnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- getmetrics.sql: +CREATE OR REPLACE FUNCTION notifications.getmetrics( + month_input int, + year_input int +) +RETURNS TABLE ( + org text, + placed_orders bigint, + sent_emails bigint, + succeeded_emails bigint, + sent_sms bigint, + succeeded_sms bigint +) +AS $$ +BEGIN + RETURN QUERY + SELECT + o.creatorname, + COUNT(DISTINCT o._id) AS placed_orders, + SUM(CASE WHEN e._id IS NOT NULL THEN 1 ELSE 0 END) AS sent_emails, + SUM(CASE WHEN e.result = 'Succeeded' THEN 1 ELSE 0 END) AS succeeded_emails, + SUM(CASE WHEN s._id IS NOT NULL THEN s.smscount ELSE 0 END) AS sent_sms, + SUM(CASE WHEN s.result = 'Accepted' THEN 1 ELSE 0 END) AS succeeded_sms + FROM notifications.orders o + LEFT JOIN notifications.emailnotifications e ON o._id = e._orderid + LEFT JOIN notifications.smsnotifications s ON o._id = s._orderid + WHERE EXTRACT(MONTH FROM o.requestedsendtime) = month_input + AND EXTRACT(YEAR FROM o.requestedsendtime) = year_input + GROUP BY o.creatorname; +END; +$$ LANGUAGE plpgsql; + + +-- getorderincludestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorder_includestatus_v2( + _alternateid uuid, + _creatorname text +) +RETURNS TABLE( + alternateid uuid, + creatorname text, + sendersreference text, + created timestamp with time zone, + requestedsendtime timestamp with time zone, + processed timestamp with time zone, + processedstatus orderprocessingstate, + notificationchannel text, + generatedemailcount bigint, + succeededemailcount bigint, + generatedsmscount bigint, + succeededsmscount bigint +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + _target_orderid INTEGER; + _succeededEmailCount BIGINT; + _generatedEmailCount BIGINT; + _succeededSmsCount BIGINT; + _generatedSmsCount BIGINT; +BEGIN + SELECT _id INTO _target_orderid + FROM notifications.orders + WHERE orders.alternateid = _alternateid + AND orders.creatorname = _creatorname; + + SELECT + SUM(CASE WHEN result = 'Succeeded' THEN 1 ELSE 0 END), + COUNT(1) AS generatedEmailCount + INTO _succeededEmailCount, _generatedEmailCount + FROM notifications.emailnotifications + WHERE _orderid = _target_orderid; + + SELECT + SUM(CASE WHEN result = 'Accepted' THEN 1 ELSE 0 END), + COUNT(1) AS generatedSmsCount + INTO _succeededSmsCount, _generatedSmsCount + FROM notifications.smsnotifications + WHERE _orderid = _target_orderid; + + RETURN QUERY + SELECT + orders.alternateid, + orders.creatorname, + orders.sendersreference, + orders.created, + orders.requestedsendtime, + orders.processed, + orders.processedstatus, + orders.notificationorder->>'NotificationChannel', + _generatedEmailCount, + _succeededEmailCount, + _generatedSmsCount, + _succeededSmsCount + FROM + notifications.orders AS orders + WHERE + orders.alternateid = _alternateid; +END; +$BODY$; + + +-- getorderspastsendtimeupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getorders_pastsendtime_updatestatus() + RETURNS TABLE(notificationorders jsonb) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +RETURN QUERY + UPDATE notifications.orders + SET processedstatus = 'Processing' + WHERE _id IN (select _id + from notifications.orders + where processedstatus = 'Registered' + and requestedsendtime <= now() + INTERVAL '1 minute' + limit 50) + RETURNING notificationorder AS notificationorders; +END; +$BODY$; + +-- getsmsrecipients.sql: +CREATE OR REPLACE FUNCTION notifications.getsmsrecipients_v2(_orderid uuid) +RETURNS TABLE( + recipientorgno text, + recipientnin text, + mobilenumber text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN +RETURN query + SELECT s.recipientorgno, s.recipientnin, s.mobilenumber + FROM notifications.smsnotifications s + WHERE s._orderid = __orderid; +END; +$BODY$; + +-- getsmsstatusnewupdatestatus.sql: +CREATE OR REPLACE FUNCTION notifications.getsms_statusnew_updatestatus() + RETURNS TABLE(alternateid uuid, sendernumber text, mobilenumber text, body text) + LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + + RETURN query + WITH updated AS ( + UPDATE notifications.smsnotifications + SET result = 'Sending', resulttime = now() + WHERE result = 'New' + RETURNING notifications.smsnotifications.alternateid, _orderid, notifications.smsnotifications.mobilenumber) + SELECT u.alternateid, st.sendernumber, u.mobilenumber, st.body + FROM updated u, notifications.smstexts st + WHERE u._orderid = st._orderid; +END; +$BODY$; + +-- getsmssummary.sql: +CREATE OR REPLACE FUNCTION notifications.getsmssummary_v2( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientorgno text, + recipientnin text, + mobilenumber text, + result smsnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientorgno, n.recipientnin, n.mobilenumber, n.result, n.resulttime + FROM notifications.smsnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::text, NULL::smsnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; + +-- insertemailnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailnotification( +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_toaddress TEXT, +_result text, +_resulttime timestamptz, +_expirytime timestamptz) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN + +INSERT INTO notifications.emailnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +toaddress, result, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_toaddress, +_result::emailnotificationresulttype, +_resulttime, +_expirytime); +END; +$BODY$; + +-- insertemailtext.sql: +CREATE OR REPLACE PROCEDURE notifications.insertemailtext(__orderid BIGINT, _fromaddress TEXT, _subject TEXT, _body TEXT, _contenttype TEXT) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN +INSERT INTO notifications.emailtexts(_orderid, fromaddress, subject, body, contenttype) + VALUES (__orderid, _fromaddress, _subject, _body, _contenttype); +END; +$BODY$; + + +-- insertorder.sql: +CREATE OR REPLACE FUNCTION notifications.insertorder(_alternateid UUID, _creatorname TEXT, _sendersreference TEXT, _created TIMESTAMPTZ, _requestedsendtime TIMESTAMPTZ, _notificationorder JSONB) +RETURNS BIGINT + LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +_orderid BIGINT; +BEGIN + INSERT INTO notifications.orders(alternateid, creatorname, sendersreference, created, requestedsendtime, processed, notificationorder) + VALUES (_alternateid, _creatorname, _sendersreference, _created, _requestedsendtime, _created, _notificationorder) + RETURNING _id INTO _orderid; + + RETURN _orderid; +END; +$BODY$; + +-- insertsmsnotification.sql: +CREATE OR REPLACE PROCEDURE notifications.insertsmsnotification( +_orderid uuid, +_alternateid uuid, +_recipientorgno TEXT, +_recipientnin TEXT, +_mobilenumber TEXT, +_result text, +_smscount integer, +_resulttime timestamptz, +_expirytime timestamptz +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN + +INSERT INTO notifications.smsnotifications( +_orderid, +alternateid, +recipientorgno, +recipientnin, +mobilenumber, +result, +smscount, +resulttime, +expirytime) +VALUES ( +__orderid, +_alternateid, +_recipientorgno, +_recipientnin, +_mobilenumber, +_result::smsnotificationresulttype, +_smscount, +_resulttime, +_expirytime); +END; +$BODY$; + +-- updateemailstatus.sql: +CREATE OR REPLACE PROCEDURE notifications.updateemailstatus(_alternateid UUID, _result text, _operationid text) +LANGUAGE 'plpgsql' +AS $BODY$ +BEGIN + UPDATE notifications.emailnotifications + SET result = _result::emailnotificationresulttype, resulttime = now(), operationid = _operationid + WHERE alternateid = _alternateid; +END; +$BODY$; + diff --git a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs index 41ff6273..46fa39d3 100644 --- a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs +++ b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs @@ -1,7 +1,5 @@ using Altinn.Notifications.Configuration; -using Altinn.Notifications.Core.Models.Orders; using Altinn.Notifications.Core.Services.Interfaces; -using Altinn.Notifications.Core.Shared; using Altinn.Notifications.Extensions; using Altinn.Notifications.Mappers; using Altinn.Notifications.Models; @@ -22,7 +20,7 @@ namespace Altinn.Notifications.Controllers; /// [Route("notifications/api/v1/orders/email")] [ApiController] -[Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)] +/// [Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)] [SwaggerResponse(401, "Caller is unauthorized")] [SwaggerResponse(403, "Caller is not authorized to access the requested resource")] @@ -63,7 +61,7 @@ public async Task> Post(EmailN return ValidationProblem(ModelState); } - string? creator = HttpContext.GetOrg(); + string? creator = "ttd"; // HttpContext.GetOrg(); if (creator == null) { diff --git a/src/Altinn.Notifications/Mappers/OrderMapper.cs b/src/Altinn.Notifications/Mappers/OrderMapper.cs index 5a6f68a8..3698165b 100644 --- a/src/Altinn.Notifications/Mappers/OrderMapper.cs +++ b/src/Altinn.Notifications/Mappers/OrderMapper.cs @@ -42,7 +42,8 @@ public static NotificationOrderRequest MapToOrderRequest(this EmailNotificationO extRequest.RequestedSendTime.ToUniversalTime(), NotificationChannel.Email, recipients, - extRequest.IgnoreReservation); + extRequest.IgnoreReservation, + extRequest.ResourceId); } /// @@ -74,7 +75,8 @@ public static NotificationOrderRequest MapToOrderRequest(this SmsNotificationOrd extRequest.RequestedSendTime.ToUniversalTime(), NotificationChannel.Sms, recipients, - extRequest.IgnoreReservation); + extRequest.IgnoreReservation, + extRequest.ResourceId); } /// @@ -213,6 +215,9 @@ private static IBaseNotificationOrderExt MapBaseNotificationOrder(this IBaseNoti orderExt.Creator = order.Creator.ShortName; orderExt.NotificationChannel = (NotificationChannelExt)order.NotificationChannel; orderExt.RequestedSendTime = order.RequestedSendTime; + orderExt.IgnoreReservation = order.IgnoreReservation; + orderExt.ResourceId = order.ResourceId; + return orderExt; } diff --git a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs index df9448b4..43bfdfdd 100644 --- a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs @@ -53,6 +53,12 @@ public class EmailNotificationOrderRequestExt [JsonPropertyName("ignoreReservation")] public bool IgnoreReservation { get; set; } + /// + /// Gets or sets the id of the resource that the notification is related to + /// + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + /// /// Json serialized the /// diff --git a/src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs b/src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs index 6de072f3..9fd9237d 100644 --- a/src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs +++ b/src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs @@ -1,4 +1,6 @@ -namespace Altinn.Notifications.Models; +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; /// /// A class representing the base properties of a registered notification order. @@ -37,4 +39,16 @@ public interface IBaseNotificationOrderExt /// Gets or sets the preferred notification channel of the notification order /// public NotificationChannelExt NotificationChannel { get; set; } + + /// + /// Gets or sets whether notifications generated by this order should ignore KRR reservations + /// + [JsonPropertyName("ignoreReservation")] + public bool IgnoreReservation { get; set; } + + /// + /// Gets or sets the id of the resource that the notification is related to + /// + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } } diff --git a/src/Altinn.Notifications/Models/NotificationOrderExt.cs b/src/Altinn.Notifications/Models/NotificationOrderExt.cs index 890ff300..0f22c9e0 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderExt.cs @@ -35,12 +35,14 @@ public class NotificationOrderExt : IBaseNotificationOrderExt [JsonConverter(typeof(JsonStringEnumConverter))] public NotificationChannelExt NotificationChannel { get; set; } - /// - /// Gets or sets whether notifications generated by this order should ignore KRR reservations - /// + /// > [JsonPropertyName("ignoreReservation")] public bool IgnoreReservation { get; set; } + /// > + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + /// /// Gets or sets the list of recipients /// diff --git a/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs b/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs index dc07c6aa..80cd21e3 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs @@ -35,6 +35,14 @@ public class NotificationOrderWithStatusExt : IBaseNotificationOrderExt [JsonConverter(typeof(JsonStringEnumConverter))] public NotificationChannelExt NotificationChannel { get; set; } + /// > + [JsonPropertyName("ignoreReservation")] + public bool IgnoreReservation { get; set; } + + /// > + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + /// /// Gets or sets the processing status of the notication order /// diff --git a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs index 0f413a3d..f5261483 100644 --- a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs @@ -47,6 +47,12 @@ public class SmsNotificationOrderRequestExt [JsonPropertyName("ignoreReservation")] public bool IgnoreReservation { get; set; } + /// + /// Gets or sets the id of the resource that the notification is related to + /// + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + /// /// Json serialized the /// diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications/OrdersController/OrdersControllerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications/OrdersController/OrdersControllerTests.cs index 6340334a..f04d638c 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications/OrdersController/OrdersControllerTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications/OrdersController/OrdersControllerTests.cs @@ -45,7 +45,8 @@ public OrdersControllerTests(IntegrationTestWebApplicationFactory(), - false); + false, + null); _orderWithStatus = new( Guid.NewGuid(), diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index 92d2ef09..705e24f6 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -43,7 +43,7 @@ public async Task AddSmsContactPoints_NationalIdentityNumberAvailable_ProfileSer var service = GetTestService(profileClient: profileClientMock.Object); // Act - await service.AddSmsContactPoints(input); + await service.AddSmsContactPoints(input, null); // Assert Assert.Equivalent(expectedOutput, input); @@ -78,7 +78,7 @@ public async Task AddSmsContactPoints_OrganizationNumberAvailable_RegisterServic var service = GetTestService(registerClient: registerClientMock.Object); // Act - await service.AddSmsContactPoints(input); + await service.AddSmsContactPoints(input, null); // Assert Assert.Equivalent(expectedOutput, input); @@ -112,7 +112,7 @@ public async Task AddEmailContactPoints_NationalIdentityNumberAvailable_ProfileS var service = GetTestService(profileClient: profileClientMock.Object); // Act - await service.AddEmailContactPoints(input); + await service.AddEmailContactPoints(input, null); // Assert Assert.Equivalent(expectedOutput, input); @@ -145,7 +145,7 @@ public async Task AddEmailContactPoints_OrganizationNumberAvailable_RegisterServ var service = GetTestService(registerClient: registerClientMock.Object); // Act - await service.AddEmailContactPoints(input); + await service.AddEmailContactPoints(input, null); // Assert Assert.Equivalent(expectedOutput, input); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs index fa990c1a..3a03818a 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -144,7 +144,7 @@ public async Task ProcessOrder_RecipientMissingEmail_ContactPointServiceCalled() It.IsAny())); var contactPointServiceMock = new Mock(); - contactPointServiceMock.Setup(c => c.AddEmailContactPoints(It.Is>(r => r.Count == 1))); + contactPointServiceMock.Setup(c => c.AddEmailContactPoints(It.Is>(r => r.Count == 1), It.IsAny())); var service = GetTestService(emailService: notificationServiceMock.Object, contactPointService: contactPointServiceMock.Object); @@ -152,7 +152,7 @@ public async Task ProcessOrder_RecipientMissingEmail_ContactPointServiceCalled() await service.ProcessOrder(order); // Assert - contactPointServiceMock.Verify(c => c.AddEmailContactPoints(It.Is>(r => r.Count == 1)), Times.Once); + contactPointServiceMock.Verify(c => c.AddEmailContactPoints(It.Is>(r => r.Count == 1), It.IsAny()), Times.Once); notificationServiceMock.VerifyAll(); } @@ -214,7 +214,7 @@ private static EmailOrderProcessingService GetTestService( { var contactPointServiceMock = new Mock(); contactPointServiceMock - .Setup(e => e.AddEmailContactPoints(It.IsAny>())); + .Setup(e => e.AddEmailContactPoints(It.IsAny>(), It.IsAny())); contactPointService = contactPointServiceMock.Object; } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs index 7f158c6c..10704b4f 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs @@ -289,7 +289,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupPartialSuccess_OrderCre Mock contactPointMock = new(); contactPointMock - .Setup(cp => cp.AddSmsContactPoints(It.IsAny>())) + .Setup(cp => cp.AddSmsContactPoints(It.IsAny>(), It.IsAny())) .Callback>(recipients => { foreach (var recipient in recipients) @@ -359,7 +359,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupSuccess_OrderCreated() Mock contactPointMock = new(); contactPointMock - .Setup(cp => cp.AddSmsContactPoints(It.IsAny>())) + .Setup(cp => cp.AddSmsContactPoints(It.IsAny>(), It.IsAny())) .Callback>(recipients => { foreach (var recipient in recipients) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs index 097e4a7a..1cf051ea 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs @@ -119,7 +119,7 @@ public async Task ProcessOrder_RecipientMissingMobileNumber_ContactPointServiceC It.IsAny())); var contactPointServiceMock = new Mock(); - contactPointServiceMock.Setup(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1))) + contactPointServiceMock.Setup(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1), It.IsAny())) .Callback>(r => { Recipient augumentedRecipient = new() { AddressInfo = [new SmsAddressPoint("+4712345678")], NationalIdentityNumber = r[0].NationalIdentityNumber }; @@ -133,7 +133,7 @@ public async Task ProcessOrder_RecipientMissingMobileNumber_ContactPointServiceC await service.ProcessOrder(order); // Assert - contactPointServiceMock.Verify(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1)), Times.Once); + contactPointServiceMock.Verify(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1), It.IsAny()), Times.Once); notificationServiceMock.VerifyAll(); } @@ -212,7 +212,7 @@ private static SmsOrderProcessingService GetTestService( if (contactPointService == null) { var contactPointServiceMock = new Mock(); - contactPointServiceMock.Setup(e => e.AddSmsContactPoints(It.IsAny>())); + contactPointServiceMock.Setup(e => e.AddSmsContactPoints(It.IsAny>(), It.IsAny())); contactPointService = contactPointServiceMock.Object; } From 1d73fa240e67e67c42b241647c1736c430e54562 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Tue, 7 May 2024 17:08:30 +0200 Subject: [PATCH 03/36] reinstated authorization in controller --- .../Controllers/EmailNotificationOrdersController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs index 46fa39d3..4948de71 100644 --- a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs +++ b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs @@ -20,7 +20,7 @@ namespace Altinn.Notifications.Controllers; /// [Route("notifications/api/v1/orders/email")] [ApiController] -/// [Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)] +[Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)] [SwaggerResponse(401, "Caller is unauthorized")] [SwaggerResponse(403, "Caller is not authorized to access the requested resource")] @@ -61,7 +61,7 @@ public async Task> Post(EmailN return ValidationProblem(ModelState); } - string? creator = "ttd"; // HttpContext.GetOrg(); + string? creator = HttpContext.GetOrg(); if (creator == null) { From 5f65e7537c56ebaadcb50eeb0ec5da6e4b7b6f11 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 8 May 2024 11:18:38 +0200 Subject: [PATCH 04/36] fixed code smells and duplication --- .../Models/Orders/IBaseNotificationOrder.cs | 4 +- .../Models/Orders/NotificationOrderRequest.cs | 4 +- .../Mappers/OrderMapper.cs | 2 +- ...rderExt.cs => BaseNotificationOrderExt.cs} | 21 +++++--- .../EmailNotificationOrderRequestExt.cs | 43 +---------------- .../Models/NotificationOrderExt.cs | 35 +------------- .../Models/NotificationOrderRequestBaseExt.cs | 48 +++++++++++++++++++ .../Models/NotificationOrderWithStatusExt.cs | 35 +------------- .../Models/SmsNotificationOrderRequestExt.cs | 40 +--------------- .../OrderRequestServiceTests.cs | 4 +- .../SmsOrderProcessingServiceTests.cs | 2 +- 11 files changed, 73 insertions(+), 165 deletions(-) rename src/Altinn.Notifications/Models/{IBaseNotificationOrderExt.cs => BaseNotificationOrderExt.cs} (78%) create mode 100644 src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs diff --git a/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs b/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs index 98f728dc..f6a71991 100644 --- a/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs +++ b/src/Altinn.Notifications.Core/Models/Orders/IBaseNotificationOrder.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Enums; namespace Altinn.Notifications.Core.Models.Orders; diff --git a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs index fa7af4d1..882f1a0a 100644 --- a/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs +++ b/src/Altinn.Notifications.Core/Models/Orders/NotificationOrderRequest.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Models.NotificationTemplate; namespace Altinn.Notifications.Core.Models.Orders; diff --git a/src/Altinn.Notifications/Mappers/OrderMapper.cs b/src/Altinn.Notifications/Mappers/OrderMapper.cs index 3698165b..3a9287c8 100644 --- a/src/Altinn.Notifications/Mappers/OrderMapper.cs +++ b/src/Altinn.Notifications/Mappers/OrderMapper.cs @@ -207,7 +207,7 @@ internal static List MapToRecipientExt(this List recipi return recipientExt; } - private static IBaseNotificationOrderExt MapBaseNotificationOrder(this IBaseNotificationOrderExt orderExt, IBaseNotificationOrder order) + private static BaseNotificationOrderExt MapBaseNotificationOrder(this BaseNotificationOrderExt orderExt, IBaseNotificationOrder order) { orderExt.Id = order.Id.ToString(); orderExt.SendersReference = order.SendersReference; diff --git a/src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs b/src/Altinn.Notifications/Models/BaseNotificationOrderExt.cs similarity index 78% rename from src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs rename to src/Altinn.Notifications/Models/BaseNotificationOrderExt.cs index 9fd9237d..c69327d4 100644 --- a/src/Altinn.Notifications/Models/IBaseNotificationOrderExt.cs +++ b/src/Altinn.Notifications/Models/BaseNotificationOrderExt.cs @@ -8,36 +8,43 @@ namespace Altinn.Notifications.Models; /// /// External representaion to be used in the API. /// -public interface IBaseNotificationOrderExt +public class BaseNotificationOrderExt { /// /// Gets or sets the id of the notification order /// - public string Id { get; set; } - - /// - /// Gets or sets the short name of the creator of the notification order - /// - public string Creator { get; set; } + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// /// Gets or sets the senders reference of the notification /// + [JsonPropertyName("sendersReference")] public string? SendersReference { get; set; } /// /// Gets or sets the requested send time of the notification /// + [JsonPropertyName("requestedSendTime")] public DateTime RequestedSendTime { get; set; } + /// + /// Gets or sets the short name of the creator of the notification order + /// + [JsonPropertyName("creator")] + public string Creator { get; set; } = string.Empty; + /// /// Gets or sets the date and time of when the notification order was created /// + [JsonPropertyName("created")] public DateTime Created { get; set; } /// /// Gets or sets the preferred notification channel of the notification order /// + [JsonPropertyName("notificationChannel")] + [JsonConverter(typeof(JsonStringEnumConverter))] public NotificationChannelExt NotificationChannel { get; set; } /// diff --git a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs index 43bfdfdd..4a35e469 100644 --- a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs @@ -1,5 +1,4 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Altinn.Notifications.Models; @@ -9,7 +8,7 @@ namespace Altinn.Notifications.Models; /// /// External representation to be used in the API /// -public class EmailNotificationOrderRequestExt +public class EmailNotificationOrderRequestExt : NotificationOrderRequestBaseExt { /// /// Gets or sets the subject of the email @@ -28,42 +27,4 @@ public class EmailNotificationOrderRequestExt /// [JsonPropertyName("contentType")] public EmailContentTypeExt ContentType { get; set; } = EmailContentTypeExt.Plain; - - /// - /// Gets or sets the send time of the email. Defaults to UtcNow - /// - [JsonPropertyName("requestedSendTime")] - public DateTime RequestedSendTime { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the senders reference on the notification - /// - [JsonPropertyName("sendersReference")] - public string? SendersReference { get; set; } - - /// - /// Gets or sets the list of recipients - /// - [JsonPropertyName("recipients")] - public List Recipients { get; set; } = new List(); - - /// - /// Gets or sets whether notifications generated by this order should ignore KRR reservations - /// - [JsonPropertyName("ignoreReservation")] - public bool IgnoreReservation { get; set; } - - /// - /// Gets or sets the id of the resource that the notification is related to - /// - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } - - /// - /// Json serialized the - /// - public string Serialize() - { - return JsonSerializer.Serialize(this); - } } diff --git a/src/Altinn.Notifications/Models/NotificationOrderExt.cs b/src/Altinn.Notifications/Models/NotificationOrderExt.cs index 0f22c9e0..4a49196b 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderExt.cs @@ -8,41 +8,8 @@ namespace Altinn.Notifications.Models; /// /// External representaion to be used in the API. /// -public class NotificationOrderExt : IBaseNotificationOrderExt +public class NotificationOrderExt : BaseNotificationOrderExt { - /// > - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// > - [JsonPropertyName("creator")] - public string Creator { get; set; } = string.Empty; - - /// > - [JsonPropertyName("sendersReference")] - public string? SendersReference { get; set; } - - /// > - [JsonPropertyName("requestedSendTime")] - public DateTime RequestedSendTime { get; set; } - - /// > - [JsonPropertyName("created")] - public DateTime Created { get; set; } - - /// > - [JsonPropertyName("notificationChannel")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public NotificationChannelExt NotificationChannel { get; set; } - - /// > - [JsonPropertyName("ignoreReservation")] - public bool IgnoreReservation { get; set; } - - /// > - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } - /// /// Gets or sets the list of recipients /// diff --git a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs new file mode 100644 index 00000000..1e1045b8 --- /dev/null +++ b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// Base class for common properties of notification order requests +/// +public class NotificationOrderRequestBaseExt +{ + /// + /// Gets or sets the send time of the email. Defaults to UtcNow + /// + [JsonPropertyName("requestedSendTime")] + public DateTime RequestedSendTime { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the senders reference on the notification + /// + [JsonPropertyName("sendersReference")] + public string? SendersReference { get; set; } + + /// + /// Gets or sets the list of recipients + /// + [JsonPropertyName("recipients")] + public List Recipients { get; set; } = new List(); + + /// + /// Gets or sets whether notifications generated by this order should ignore KRR reservations + /// + [JsonPropertyName("ignoreReservation")] + public bool IgnoreReservation { get; set; } + + /// + /// Gets or sets the id of the resource that the notification is related to + /// + [JsonPropertyName("resourceId")] + public string? ResourceId { get; set; } + + /// + /// Json serialized the + /// + public string Serialize() + { + return JsonSerializer.Serialize(this); + } +} diff --git a/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs b/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs index 80cd21e3..46c22ced 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs @@ -8,41 +8,8 @@ namespace Altinn.Notifications.Models; /// /// External representation to be used in the API. /// -public class NotificationOrderWithStatusExt : IBaseNotificationOrderExt +public class NotificationOrderWithStatusExt : BaseNotificationOrderExt { - /// > - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - /// > - [JsonPropertyName("sendersReference")] - public string? SendersReference { get; set; } - - /// > - [JsonPropertyName("requestedSendTime")] - public DateTime RequestedSendTime { get; set; } - - /// > - [JsonPropertyName("creator")] - public string Creator { get; set; } = string.Empty; - - /// > - [JsonPropertyName("created")] - public DateTime Created { get; set; } - - /// > - [JsonPropertyName("notificationChannel")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public NotificationChannelExt NotificationChannel { get; set; } - - /// > - [JsonPropertyName("ignoreReservation")] - public bool IgnoreReservation { get; set; } - - /// > - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } - /// /// Gets or sets the processing status of the notication order /// diff --git a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs index f5261483..ed79efa9 100644 --- a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs @@ -9,7 +9,7 @@ namespace Altinn.Notifications.Models; /// /// External representation to be used in the API. /// -public class SmsNotificationOrderRequestExt +public class SmsNotificationOrderRequestExt : NotificationOrderRequestBaseExt { /// /// Gets or sets the sender number of the SMS @@ -22,42 +22,4 @@ public class SmsNotificationOrderRequestExt /// [JsonPropertyName("body")] public string Body { get; set; } = string.Empty; - - /// - /// Gets or sets the send time of the SMS. Defaults to UtcNow. - /// - [JsonPropertyName("requestedSendTime")] - public DateTime RequestedSendTime { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the senders reference on the notification - /// - [JsonPropertyName("sendersReference")] - public string? SendersReference { get; set; } - - /// - /// Gets or sets the list of recipients - /// - [JsonPropertyName("recipients")] - public List Recipients { get; set; } = new List(); - - /// - /// Gets or sets whether notifications generated by this order should ignore KRR reservations - /// - [JsonPropertyName("ignoreReservation")] - public bool IgnoreReservation { get; set; } - - /// - /// Gets or sets the id of the resource that the notification is related to - /// - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } - - /// - /// Json serialized the - /// - public string Serialize() - { - return JsonSerializer.Serialize(this); - } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs index 10704b4f..d7e8bb9b 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderRequestServiceTests.cs @@ -290,7 +290,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupPartialSuccess_OrderCre Mock contactPointMock = new(); contactPointMock .Setup(cp => cp.AddSmsContactPoints(It.IsAny>(), It.IsAny())) - .Callback>(recipients => + .Callback, string?>((recipients, _) => { foreach (var recipient in recipients) { @@ -360,7 +360,7 @@ public async Task RegisterNotificationOrder_ForSms_LookupSuccess_OrderCreated() Mock contactPointMock = new(); contactPointMock .Setup(cp => cp.AddSmsContactPoints(It.IsAny>(), It.IsAny())) - .Callback>(recipients => + .Callback, string?>((recipients, _) => { foreach (var recipient in recipients) { diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs index 1cf051ea..c53b205d 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs @@ -120,7 +120,7 @@ public async Task ProcessOrder_RecipientMissingMobileNumber_ContactPointServiceC var contactPointServiceMock = new Mock(); contactPointServiceMock.Setup(c => c.AddSmsContactPoints(It.Is>(r => r.Count == 1), It.IsAny())) - .Callback>(r => + .Callback, string?>((r, _) => { Recipient augumentedRecipient = new() { AddressInfo = [new SmsAddressPoint("+4712345678")], NationalIdentityNumber = r[0].NationalIdentityNumber }; r.Clear(); From c3c7811f7b0ee1174d22d2949cc83cbac20168fa Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 8 May 2024 11:28:49 +0200 Subject: [PATCH 05/36] moved serialization out of base class --- .../Models/EmailNotificationOrderRequestExt.cs | 11 ++++++++++- .../Models/NotificationOrderRequestBaseExt.cs | 8 -------- .../Models/SmsNotificationOrderRequestExt.cs | 8 ++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs index 4a35e469..a6569b96 100644 --- a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Altinn.Notifications.Models; @@ -27,4 +28,12 @@ public class EmailNotificationOrderRequestExt : NotificationOrderRequestBaseExt /// [JsonPropertyName("contentType")] public EmailContentTypeExt ContentType { get; set; } = EmailContentTypeExt.Plain; + + /// + /// Json serialized the + /// + public string Serialize() + { + return JsonSerializer.Serialize(this); + } } diff --git a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs index 1e1045b8..fbc57ff1 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs @@ -37,12 +37,4 @@ public class NotificationOrderRequestBaseExt /// [JsonPropertyName("resourceId")] public string? ResourceId { get; set; } - - /// - /// Json serialized the - /// - public string Serialize() - { - return JsonSerializer.Serialize(this); - } } diff --git a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs index ed79efa9..248d05ba 100644 --- a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs @@ -22,4 +22,12 @@ public class SmsNotificationOrderRequestExt : NotificationOrderRequestBaseExt /// [JsonPropertyName("body")] public string Body { get; set; } = string.Empty; + + /// + /// Json serialized the + /// + public string Serialize() + { + return JsonSerializer.Serialize(this); + } } From b6fb3459dcbcf9f81da7fd020dad0c205e78a1fc Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 8 May 2024 11:34:28 +0200 Subject: [PATCH 06/36] using find instead of firstordefault --- src/Altinn.Notifications.Core/Services/ContactPointService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index cd963857..03abc756 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -179,7 +179,7 @@ private async Task> LookupOrganizationContactPoi userContactPoint.MobileNumber = MobileNumberHelper.EnsureCountryCodeIfValidNumber(userContactPoint.MobileNumber); }); - var existingContactPoint = contactPoints.FirstOrDefault(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); + var existingContactPoint = contactPoints.Find(cp => cp.OrganizationNumber == userContactPoint.OrganizationNumber); if (existingContactPoint != null) { From f438b23b1ddd0cff6903a5d8edb471dd3a33738f Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Fri, 10 May 2024 16:16:46 +0200 Subject: [PATCH 07/36] Authorization for multiple users in many organizations --- .../Authorization/AuthorizationService.cs | 95 +++++++++++++------ .../Authorization/IAuthorizationService.cs | 5 +- .../Controllers/TestController.cs | 29 ++++-- 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/src/Altinn.Notifications/Authorization/AuthorizationService.cs b/src/Altinn.Notifications/Authorization/AuthorizationService.cs index 53eab63c..15f6f8e9 100644 --- a/src/Altinn.Notifications/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications/Authorization/AuthorizationService.cs @@ -1,11 +1,11 @@ -using System.Security.Claims; -using System.Text.Json; +using System.Collections.Generic; +using System.Security.Claims; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Constants; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; - +using Altinn.Notifications.Core.Enums; using static Altinn.Authorization.ABAC.Constants.XacmlConstants; namespace Altinn.Notifications.Authorization; @@ -20,7 +20,7 @@ public class AuthorizationService : IAuthorizationService private const string DefaultIssuer = "Altinn"; private const string ActionCategoryId = "action"; - private const string ResourceCategoryId = "resource"; + private const string ResourceCategoryIdPrefix = "resource"; private const string AccessSubjectCategoryIdPrefix = "subject"; private readonly IPDP _pdp; @@ -37,49 +37,88 @@ public AuthorizationService(IPDP pdp) /// An implementation of that /// will generate an authorization call to Altinn Authorization to check that the given users have read access. /// - /// The list of user ids. + /// The list organizations with associated right holders. /// The id of the resource. - /// The party id of the resource owner. /// A task - public async Task> AuthorizeUsersForResource(List userIds, string resourceId, int resourceOwnerId) + public async Task>> AuthorizeUsersForResource(Dictionary> orgRightHolders, string resourceId) { XacmlJsonRequest request = new() { AccessSubject = [], Action = [CreateActionCategory()], - Resource = [CreateResourceCategory(resourceId, resourceOwnerId)], + Resource = [], MultiRequests = new XacmlJsonMultiRequests { RequestReference = [] } }; - foreach (int userId in userIds.Distinct()) + foreach (var organization in orgRightHolders) { - XacmlJsonCategory subjectCategory = CreateAccessSubjectCategory(userId); - request.AccessSubject.Add(subjectCategory); - request.MultiRequests.RequestReference.Add(CreateRequestReference(subjectCategory.Id)); + XacmlJsonCategory resourceCategory = CreateResourceCategory(organization.Key, resourceId); + + if (request.Resource.All(rc => rc.Id != resourceCategory.Id)) + { + request.Resource.Add(resourceCategory); + } + + foreach (int userId in organization.Value.Distinct()) + { + XacmlJsonCategory subjectCategory = CreateAccessSubjectCategory(userId); + + if (request.AccessSubject.All(sc => sc.Id != subjectCategory.Id)) + { + request.AccessSubject.Add(subjectCategory); + } + + request.MultiRequests.RequestReference.Add(CreateRequestReference(resourceCategory.Id, subjectCategory.Id)); + } } XacmlJsonRequestRoot jsonRequest = new() { Request = request }; XacmlJsonResponse xacmlJsonResponse = await _pdp.GetDecisionForRequest(jsonRequest); - Dictionary keyValuePairs = []; + Dictionary> permit = []; - foreach (var response in xacmlJsonResponse.Response) + foreach (var response in xacmlJsonResponse.Response.Where(r => r.Decision == "Permit")) { - XacmlJsonCategory? xacmlJsonCategory = response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Subject); + XacmlJsonCategory? resourceCategory = + response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Resource); + + string? partyId = null; + + if (resourceCategory is not null) + { + XacmlJsonAttribute? partyAttribute = + resourceCategory.Attribute.Find(a => a.AttributeId == AltinnXacmlUrns.PartyId); + + if (partyAttribute is not null) + { + partyId = partyAttribute.Value; + } + } - if (xacmlJsonCategory is not null) + XacmlJsonCategory? subjectCategory = + response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Subject); + + if (subjectCategory is not null) { - XacmlJsonAttribute? xacmlJsonAttribute = xacmlJsonCategory.Attribute.Find(a => a.AttributeId == UserIdUrn); + XacmlJsonAttribute? userAttribute + = subjectCategory.Attribute.Find(a => a.AttributeId == UserIdUrn); - if (xacmlJsonAttribute is not null) + if (userAttribute is not null && partyId is not null) { - keyValuePairs.Add(xacmlJsonAttribute.Value, true); + if (permit.ContainsKey(partyId)) + { + permit[partyId].Add(userAttribute.Value, true); + } + else + { + permit.Add(partyId, new Dictionary { { userAttribute.Value, true } }); + } } } } - return keyValuePairs; + return permit; } private XacmlJsonCategory CreateActionCategory() @@ -95,11 +134,13 @@ private XacmlJsonCategory CreateActionCategory() }; } - private static XacmlJsonCategory CreateResourceCategory(string resourceId, int resourceOwnerId) + private static XacmlJsonCategory CreateResourceCategory(int resourceOwnerId, string resourceId) { XacmlJsonAttribute subjectAttribute = DecisionHelper.CreateXacmlJsonAttribute( - AltinnXacmlUrns.PartyId, resourceOwnerId.ToString(), ClaimValueTypes.String, DefaultIssuer); + AltinnXacmlUrns.PartyId, resourceOwnerId.ToString(), ClaimValueTypes.String, DefaultIssuer, true); + + string resourceCategoryId = ResourceCategoryIdPrefix + resourceOwnerId; if (resourceId.StartsWith("app_")) { @@ -115,7 +156,7 @@ private static XacmlJsonCategory CreateResourceCategory(string resourceId, int r return new XacmlJsonCategory() { - Id = ResourceCategoryId, + Id = resourceCategoryId, Attribute = [orgAttribute, appAttribute, subjectAttribute] }; } @@ -126,7 +167,7 @@ private static XacmlJsonCategory CreateResourceCategory(string resourceId, int r return new XacmlJsonCategory() { - Id = ResourceCategoryId, + Id = resourceCategoryId, Attribute = [resourceAttribute, subjectAttribute] }; } @@ -144,15 +185,15 @@ private XacmlJsonCategory CreateAccessSubjectCategory(int userId) }; } - private static XacmlJsonRequestReference CreateRequestReference(string subjectCategoryId) + private static XacmlJsonRequestReference CreateRequestReference(string resourceCategoryId, string subjectCategoryId) { return new XacmlJsonRequestReference { ReferenceId = new List { subjectCategoryId, - ActionCategoryId, - ResourceCategoryId + ActionCategoryId, + resourceCategoryId } }; } diff --git a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs b/src/Altinn.Notifications/Authorization/IAuthorizationService.cs index 0796da7a..039bef5a 100644 --- a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs +++ b/src/Altinn.Notifications/Authorization/IAuthorizationService.cs @@ -12,9 +12,8 @@ public interface IAuthorizationService /// Describes a method that can create an authorization request to authorize a set of /// users for access to a resource. /// - /// The list of user ids. + /// The list organizations with associated right holders. /// The id of the resource. - /// The party id of the resource owner. /// A task - Task> AuthorizeUsersForResource(List userIds, string resourceId, int resourceOwnerId); + Task>> AuthorizeUsersForResource(Dictionary> orgRightHolders, string resourceId); } diff --git a/src/Altinn.Notifications/Controllers/TestController.cs b/src/Altinn.Notifications/Controllers/TestController.cs index 3e378dd0..73ccb9fd 100644 --- a/src/Altinn.Notifications/Controllers/TestController.cs +++ b/src/Altinn.Notifications/Controllers/TestController.cs @@ -20,9 +20,15 @@ public class TestController(Authorization.IAuthorizationService authorizationSer /// Test method for authorization. /// [HttpPost] - public async Task> Authorize([FromBody]AuthZ authz) + public async Task>> Authorize([FromBody]AuthZ authz) { - return await _authorizationService.AuthorizeUsersForResource(authz.UserIds, authz.ResourceId, authz.ResourceOwnerId); + Dictionary> orgRightHolders = new(); + foreach (var item in authz.OrgRightHolders) + { + orgRightHolders.Add(item.ResourceOwnerId, item.UserIds); + } + + return await _authorizationService.AuthorizeUsersForResource(orgRightHolders, authz.ResourceId); } } @@ -32,18 +38,29 @@ public async Task> Authorize([FromBody]AuthZ authz) public class AuthZ { /// - /// List of users + /// Resource /// - public List UserIds { get; set; } = []; + public string ResourceId { get; set; } = string.Empty; /// /// Resource /// - public string ResourceId { get; set; } = string.Empty; + public List OrgRightHolders { get; set; } = []; + } + /// + /// Represent a single resource owner and a list of potential notification recipients. + /// + public class OrgRightHolders + { /// - /// Resource + /// The owner of a given resource. /// public int ResourceOwnerId { get; set; } + + /// + /// List of users + /// + public List UserIds { get; set; } = []; } } From 4bddd5b66c382444c55406808c82f867dbcfe491 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Tue, 14 May 2024 15:40:22 +0200 Subject: [PATCH 08/36] Add tests and remove test controller --- .../Authorization/AuthorizationService.cs | 13 +- .../Authorization/IAuthorizationService.cs | 4 +- .../Controllers/TestController.cs | 66 --------- src/Altinn.Notifications/appsettings.json | 4 +- .../Altinn.Notifications.Tests.csproj | 1 + .../AuthorizationServiceTests.cs | 116 +++++++++++++++ .../TestData/TestDataLoader.cs | 34 +++++ .../XacmlJsonRequestRoot/DenyAll.json | 125 ++++++++++++++++ .../XacmlJsonRequestRoot/DenyOne.json | 125 ++++++++++++++++ .../XacmlJsonRequestRoot/PermitAll.json | 125 ++++++++++++++++ .../TestData/XacmlJsonResponse/DenyAll.json | 126 ++++++++++++++++ .../TestData/XacmlJsonResponse/DenyOne.json | 126 ++++++++++++++++ .../TestData/XacmlJsonResponse/PermitAll.json | 139 ++++++++++++++++++ 13 files changed, 926 insertions(+), 78 deletions(-) delete mode 100644 src/Altinn.Notifications/Controllers/TestController.cs create mode 100644 test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs create mode 100644 test/Altinn.Notifications.Tests/TestData/TestDataLoader.cs create mode 100644 test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyAll.json create mode 100644 test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyOne.json create mode 100644 test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/PermitAll.json create mode 100644 test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json create mode 100644 test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyOne.json create mode 100644 test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/PermitAll.json diff --git a/src/Altinn.Notifications/Authorization/AuthorizationService.cs b/src/Altinn.Notifications/Authorization/AuthorizationService.cs index 15f6f8e9..a4008f40 100644 --- a/src/Altinn.Notifications/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications/Authorization/AuthorizationService.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using System.Security.Claims; +using System.Security.Claims; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Constants; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; -using Altinn.Notifications.Core.Enums; + using static Altinn.Authorization.ABAC.Constants.XacmlConstants; namespace Altinn.Notifications.Authorization; @@ -80,14 +79,14 @@ public async Task>> AuthorizeUsersFo foreach (var response in xacmlJsonResponse.Response.Where(r => r.Decision == "Permit")) { - XacmlJsonCategory? resourceCategory = + XacmlJsonCategory? resourceCategory = response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Resource); string? partyId = null; if (resourceCategory is not null) { - XacmlJsonAttribute? partyAttribute = + XacmlJsonAttribute? partyAttribute = resourceCategory.Attribute.Find(a => a.AttributeId == AltinnXacmlUrns.PartyId); if (partyAttribute is not null) @@ -96,12 +95,12 @@ public async Task>> AuthorizeUsersFo } } - XacmlJsonCategory? subjectCategory = + XacmlJsonCategory? subjectCategory = response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Subject); if (subjectCategory is not null) { - XacmlJsonAttribute? userAttribute + XacmlJsonAttribute? userAttribute = subjectCategory.Attribute.Find(a => a.AttributeId == UserIdUrn); if (userAttribute is not null && partyId is not null) diff --git a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs b/src/Altinn.Notifications/Authorization/IAuthorizationService.cs index 039bef5a..bedbf4d0 100644 --- a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs +++ b/src/Altinn.Notifications/Authorization/IAuthorizationService.cs @@ -1,6 +1,4 @@ -using Altinn.Authorization.ABAC.Xacml.JsonProfile; - -namespace Altinn.Notifications.Authorization; +namespace Altinn.Notifications.Authorization; /// /// Describes the necessary functions of an authorization service that can perform diff --git a/src/Altinn.Notifications/Controllers/TestController.cs b/src/Altinn.Notifications/Controllers/TestController.cs deleted file mode 100644 index 73ccb9fd..00000000 --- a/src/Altinn.Notifications/Controllers/TestController.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Altinn.Authorization.ABAC.Xacml.JsonProfile; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Altinn.Notifications.Controllers -{ - /// - /// Temporary adding a controller in order to generate som test data from Authorization. - /// - [ApiController] - [Route("notifications/api/v1/test")] - [AllowAnonymous] - public class TestController(Authorization.IAuthorizationService authorizationService) - : ControllerBase - { - private readonly Authorization.IAuthorizationService _authorizationService = authorizationService; - - /// - /// Test method for authorization. - /// - [HttpPost] - public async Task>> Authorize([FromBody]AuthZ authz) - { - Dictionary> orgRightHolders = new(); - foreach (var item in authz.OrgRightHolders) - { - orgRightHolders.Add(item.ResourceOwnerId, item.UserIds); - } - - return await _authorizationService.AuthorizeUsersForResource(orgRightHolders, authz.ResourceId); - } - } - - /// - /// Request input - /// - public class AuthZ - { - /// - /// Resource - /// - public string ResourceId { get; set; } = string.Empty; - - /// - /// Resource - /// - public List OrgRightHolders { get; set; } = []; - } - - /// - /// Represent a single resource owner and a list of potential notification recipients. - /// - public class OrgRightHolders - { - /// - /// The owner of a given resource. - /// - public int ResourceOwnerId { get; set; } - - /// - /// List of users - /// - public List UserIds { get; set; } = []; - } -} diff --git a/src/Altinn.Notifications/appsettings.json b/src/Altinn.Notifications/appsettings.json index dcdebf67..749ab40d 100644 --- a/src/Altinn.Notifications/appsettings.json +++ b/src/Altinn.Notifications/appsettings.json @@ -1,8 +1,8 @@ { "PlatformSettings": { "ApiProfileEndpoint": "http://localhost:5030/profile/api/v1/", - "ApiRegisterEndpoint": "http://localhost:5020/register/api/v1/" - "ApiAuthorizationEndpoint": "https://platform.at24.altinn.cloud/authorization/api/v1/", + "ApiRegisterEndpoint": "http://localhost:5020/register/api/v1/", + "ApiAuthorizationEndpoint": "http://localhost:5030/authorization/api/v1/" }, "PostgreSQLSettings": { "MigrationScriptPath": "Migration", diff --git a/test/Altinn.Notifications.Tests/Altinn.Notifications.Tests.csproj b/test/Altinn.Notifications.Tests/Altinn.Notifications.Tests.csproj index b1d75a16..550ceaa6 100644 --- a/test/Altinn.Notifications.Tests/Altinn.Notifications.Tests.csproj +++ b/test/Altinn.Notifications.Tests/Altinn.Notifications.Tests.csproj @@ -13,6 +13,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs new file mode 100644 index 00000000..95672261 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Interfaces; +using Altinn.Notifications.Authorization; +using Altinn.Notifications.Tests.TestData; + +using FluentAssertions; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications; + +public class AuthorizationServiceTests +{ + private Mock _pdpMock = new Mock(); + + private AuthorizationService _target; + + public AuthorizationServiceTests() + { + _target = new AuthorizationService(_pdpMock.Object); + } + + [Fact] + public async Task AuthorizeUsersForResource_PermitAll() + { + // Arrange + Dictionary> input = new() + { + { 51326783, new List { 20020164 } }, + { 51529389, new List { 20020106, 20020164 } } + }; + + XacmlJsonRequestRoot? actualRequest = null; + _pdpMock.Setup(pdp => pdp.GetDecisionForRequest(It.IsAny())) + .Callback((XacmlJsonRequestRoot request) => actualRequest = request) + .ReturnsAsync(await TestDataLoader.Load("PermitAll")); + + // Act + Dictionary> actualResult = + await _target.AuthorizeUsersForResource(input, "app_ttd_apps-test"); + + // Assert + XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("PermitAll"); + actualRequest.Should().BeEquivalentTo(expectedRequest); + + Dictionary> expectedResult = new() + { + { "51326783", new Dictionary() { { "20020164", true } } }, + { "51529389", new Dictionary() { { "20020106", true }, { "20020164", true } } } + }; + actualResult.Should().BeEquivalentTo(expectedResult); + } + + [Fact] + public async Task AuthorizeUsersForResource_DenyOne() + { + // Arrange + Dictionary> input = new() + { + { 51326783, new List { 20020164 } }, + { 51529389, new List { 20020168, 20020164 } } + }; + + XacmlJsonRequestRoot? actualRequest = null; + _pdpMock.Setup(pdp => pdp.GetDecisionForRequest(It.IsAny())) + .Callback((XacmlJsonRequestRoot request) => actualRequest = request) + .ReturnsAsync(await TestDataLoader.Load("DenyOne")); + + // Act + Dictionary> actualResult = + await _target.AuthorizeUsersForResource(input, "app_ttd_apps-test"); + + // Assert + XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyOne"); + actualRequest.Should().BeEquivalentTo(expectedRequest); + + Dictionary> expectedResult = new() + { + { "51326783", new Dictionary() { { "20020164", true } } }, + { "51529389", new Dictionary() { { "20020164", true } } } + }; + actualResult.Should().BeEquivalentTo(expectedResult); + } + + [Fact] + public async Task AuthorizeUsersForResource_DenyAll() + { + // Arrange + Dictionary> input = new() + { + { 51326783, new List { 55555555 } }, + { 51529389, new List { 55555555, 66666666 } } + }; + + XacmlJsonRequestRoot? actualRequest = null; + _pdpMock.Setup(pdp => pdp.GetDecisionForRequest(It.IsAny())) + .Callback((XacmlJsonRequestRoot request) => actualRequest = request) + .ReturnsAsync(await TestDataLoader.Load("DenyAll")); + + // Act + Dictionary> actualResult = + await _target.AuthorizeUsersForResource(input, "app_ttd_apps-test"); + + // Assert + XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyAll"); + actualRequest.Should().BeEquivalentTo(expectedRequest); + + Dictionary> expectedResult = []; // Empty + actualResult.Should().BeEquivalentTo(expectedResult); + } +} diff --git a/test/Altinn.Notifications.Tests/TestData/TestDataLoader.cs b/test/Altinn.Notifications.Tests/TestData/TestDataLoader.cs new file mode 100644 index 00000000..da477ec5 --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/TestDataLoader.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Altinn.Notifications.Tests.TestData; + +public static class TestDataLoader +{ + private static JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true + }; + + public static async Task Load(string id) + { + string path = GetPath(id); + string fileContent = await File.ReadAllTextAsync(path); + T? data = JsonSerializer.Deserialize(fileContent, _options); + return data!; + } + + private static string GetPath(string id) + { + string? unitTestFolder = Path.GetDirectoryName(new Uri(typeof(T).Assembly.Location).LocalPath); + + if (unitTestFolder is null) + { + return string.Empty; + } + + return Path.Combine(unitTestFolder, "..", "..", "..", "TestData", typeof(T).Name, $"{id}.json"); + } +} diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyAll.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyAll.json new file mode 100644 index 00000000..2ebc1fbc --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyAll.json @@ -0,0 +1,125 @@ +{ + "request": { + "returnPolicyIdList": false, + "combinedDecision": false, + "resource": [ + { + "id": "resource51326783", + "attribute": [ + { + "attributeId": "urn:altinn:org", + "value": "ttd", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:app", + "value": "apps-test", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:partyid", + "value": "51326783", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + }, + { + "id": "resource51529389", + "attribute": [ + { + "attributeId": "urn:altinn:org", + "value": "ttd", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:app", + "value": "apps-test", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + } + ], + "action": [ + { + "id": "action", + "attribute": [ + { + "attributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "value": "read", + "issuer": "Altinn", + "dataType": "string", + "includeInResult": false + } + ] + } + ], + "accessSubject": [ + { + "id": "subject55555555", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "55555555", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + }, + { + "id": "subject66666666", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "66666666", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + } + ], + "multiRequests": { + "requestReference": [ + { + "referenceId": [ + "subject55555555", + "action", + "resource51326783" + ] + }, + { + "referenceId": [ + "subject55555555", + "action", + "resource51529389" + ] + }, + { + "referenceId": [ + "subject66666666", + "action", + "resource51529389" + ] + } + ] + } + } +} diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyOne.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyOne.json new file mode 100644 index 00000000..0b1df917 --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/DenyOne.json @@ -0,0 +1,125 @@ +{ + "request": { + "returnPolicyIdList": false, + "combinedDecision": false, + "resource": [ + { + "id": "resource51326783", + "attribute": [ + { + "attributeId": "urn:altinn:org", + "value": "ttd", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:app", + "value": "apps-test", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:partyid", + "value": "51326783", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + }, + { + "id": "resource51529389", + "attribute": [ + { + "attributeId": "urn:altinn:org", + "value": "ttd", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:app", + "value": "apps-test", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + } + ], + "action": [ + { + "id": "action", + "attribute": [ + { + "attributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "value": "read", + "issuer": "Altinn", + "dataType": "string", + "includeInResult": false + } + ] + } + ], + "accessSubject": [ + { + "id": "subject20020164", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + }, + { + "id": "subject20020168", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020168", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + } + ], + "multiRequests": { + "requestReference": [ + { + "referenceId": [ + "subject20020164", + "action", + "resource51326783" + ] + }, + { + "referenceId": [ + "subject20020168", + "action", + "resource51529389" + ] + }, + { + "referenceId": [ + "subject20020164", + "action", + "resource51529389" + ] + } + ] + } + } +} diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/PermitAll.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/PermitAll.json new file mode 100644 index 00000000..0480c081 --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonRequestRoot/PermitAll.json @@ -0,0 +1,125 @@ +{ + "request": { + "returnPolicyIdList": false, + "combinedDecision": false, + "resource": [ + { + "id": "resource51326783", + "attribute": [ + { + "attributeId": "urn:altinn:org", + "value": "ttd", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:app", + "value": "apps-test", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:partyid", + "value": "51326783", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + }, + { + "id": "resource51529389", + "attribute": [ + { + "attributeId": "urn:altinn:org", + "value": "ttd", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:app", + "value": "apps-test", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + }, + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + } + ], + "action": [ + { + "id": "action", + "attribute": [ + { + "attributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "value": "read", + "issuer": "Altinn", + "dataType": "string", + "includeInResult": false + } + ] + } + ], + "accessSubject": [ + { + "id": "subject20020164", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + }, + { + "id": "subject20020106", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020106", + "issuer": "Altinn", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": true + } + ] + } + ], + "multiRequests": { + "requestReference": [ + { + "referenceId": [ + "subject20020164", + "action", + "resource51326783" + ] + }, + { + "referenceId": [ + "subject20020106", + "action", + "resource51529389" + ] + }, + { + "referenceId": [ + "subject20020164", + "action", + "resource51529389" + ] + } + ] + } + } +} diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json new file mode 100644 index 00000000..409700a1 --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json @@ -0,0 +1,126 @@ +{ + "response": [ + { + "decision": "NotApplicable", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51326783", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + }, + { + "decision": "NotApplicable", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020168", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + }, + { + "decision": "NotApplicable", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyOne.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyOne.json new file mode 100644 index 00000000..9969bc82 --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyOne.json @@ -0,0 +1,126 @@ +{ + "response": [ + { + "decision": "Permit", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51326783", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + }, + { + "decision": "NotApplicable", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020168", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + }, + { + "decision": "Permit", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/PermitAll.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/PermitAll.json new file mode 100644 index 00000000..6409069c --- /dev/null +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/PermitAll.json @@ -0,0 +1,139 @@ +{ + "response": [ + { + "decision": "Permit", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51326783", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + }, + { + "decision": "Permit", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020106", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + }, + { + "decision": "Permit", + "status": { + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok" + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:authenticationLevel1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation1-assignment1", + "value": "2", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer" + } + ] + } + ], + "category": [ + { + "categoryId": "urn:oasis:names:tc:xacml:1.0:subject-category:access-subject", + "attribute": [ + { + "attributeId": "urn:altinn:userid", + "value": "20020164", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + }, + { + "categoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "attribute": [ + { + "attributeId": "urn:altinn:partyid", + "value": "51529389", + "dataType": "http://www.w3.org/2001/XMLSchema#string", + "includeInResult": false + } + ] + } + ] + } + ] +} From e22f45a47fa0372d7c222fb71a7a9feda1f025aa Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Tue, 14 May 2024 16:03:06 +0200 Subject: [PATCH 09/36] Replace ContainsKey with TryGetValue --- .../Authorization/AuthorizationService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.Notifications/Authorization/AuthorizationService.cs b/src/Altinn.Notifications/Authorization/AuthorizationService.cs index a4008f40..749f1053 100644 --- a/src/Altinn.Notifications/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications/Authorization/AuthorizationService.cs @@ -105,9 +105,9 @@ public async Task>> AuthorizeUsersFo if (userAttribute is not null && partyId is not null) { - if (permit.ContainsKey(partyId)) + if (permit.TryGetValue(partyId, out Dictionary? value)) { - permit[partyId].Add(userAttribute.Value, true); + value.Add(userAttribute.Value, true); } else { From a876e669dc75d57a465c8ac6fa9fa8101a7e8623 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 15 May 2024 13:02:39 +0200 Subject: [PATCH 10/36] Move authorization interface and service --- .../Integrations}/IAuthorizationService.cs | 2 +- .../Altinn.Notifications.Integrations.csproj | 1 + .../Authorization/AuthorizationService.cs | 15 +++++++-------- src/Altinn.Notifications/Program.cs | 3 ++- .../Notifications/AuthorizationServiceTests.cs | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) rename src/{Altinn.Notifications/Authorization => Altinn.Notifications.Core/Integrations}/IAuthorizationService.cs (93%) rename src/{Altinn.Notifications => Altinn.Notifications.Integrations}/Authorization/AuthorizationService.cs (96%) diff --git a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs similarity index 93% rename from src/Altinn.Notifications/Authorization/IAuthorizationService.cs rename to src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs index bedbf4d0..fff67f99 100644 --- a/src/Altinn.Notifications/Authorization/IAuthorizationService.cs +++ b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs @@ -1,4 +1,4 @@ -namespace Altinn.Notifications.Authorization; +namespace Altinn.Notifications.Core.Integrations; /// /// Describes the necessary functions of an authorization service that can perform diff --git a/src/Altinn.Notifications.Integrations/Altinn.Notifications.Integrations.csproj b/src/Altinn.Notifications.Integrations/Altinn.Notifications.Integrations.csproj index 07254d89..e22d92e1 100644 --- a/src/Altinn.Notifications.Integrations/Altinn.Notifications.Integrations.csproj +++ b/src/Altinn.Notifications.Integrations/Altinn.Notifications.Integrations.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Altinn.Notifications/Authorization/AuthorizationService.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs similarity index 96% rename from src/Altinn.Notifications/Authorization/AuthorizationService.cs rename to src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs index 749f1053..6b34b52d 100644 --- a/src/Altinn.Notifications/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs @@ -1,13 +1,12 @@ using System.Security.Claims; - using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Constants; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; - +using Altinn.Notifications.Core.Integrations; using static Altinn.Authorization.ABAC.Constants.XacmlConstants; -namespace Altinn.Notifications.Authorization; +namespace Altinn.Notifications.Integrations.Authorization; /// /// An implementation of able to check that a potential @@ -187,13 +186,13 @@ private XacmlJsonCategory CreateAccessSubjectCategory(int userId) private static XacmlJsonRequestReference CreateRequestReference(string resourceCategoryId, string subjectCategoryId) { return new XacmlJsonRequestReference - { - ReferenceId = new List - { - subjectCategoryId, + { + ReferenceId = new List + { + subjectCategoryId, ActionCategoryId, resourceCategoryId - } + } }; } } diff --git a/src/Altinn.Notifications/Program.cs b/src/Altinn.Notifications/Program.cs index cdd8965a..94f256a6 100644 --- a/src/Altinn.Notifications/Program.cs +++ b/src/Altinn.Notifications/Program.cs @@ -14,6 +14,7 @@ using Altinn.Notifications.Core.Extensions; using Altinn.Notifications.Extensions; using Altinn.Notifications.Health; +using Altinn.Notifications.Integrations.Authorization; using Altinn.Notifications.Integrations.Extensions; using Altinn.Notifications.Middleware; using Altinn.Notifications.Models; @@ -188,7 +189,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.Configure(config.GetSection("PlatformSettings")); services.AddHttpClient(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddKafkaServices(config); services.AddAltinnClients(config); diff --git a/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs index 95672261..805d0b89 100644 --- a/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs @@ -3,7 +3,7 @@ using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; -using Altinn.Notifications.Authorization; +using Altinn.Notifications.Integrations.Authorization; using Altinn.Notifications.Tests.TestData; using FluentAssertions; From 1bdf71af3746cf682bf6345f21f202ef1bcc02c2 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 15 May 2024 13:12:44 +0200 Subject: [PATCH 11/36] Expand OrganizationContactPoint with personal contact info --- .../Models/ContactPoints/OrganizationContactPoints.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs index a95d8cf3..45ebde36 100644 --- a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs +++ b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs @@ -10,6 +10,11 @@ public class OrganizationContactPoints /// public string OrganizationNumber { get; set; } = string.Empty; + /// + /// Gets or sets the party id of the organization + /// + public int PartyId { get; set; } + /// /// Gets or sets a list of official mobile numbers /// @@ -19,4 +24,9 @@ public class OrganizationContactPoints /// Gets or sets a list of official email addresses /// public List EmailList { get; set; } = []; + + /// + /// Gets or sets a list of user registered contanct points associated with the organisation. + /// + public List UserContactPoints { get; set; } = []; } From b6a243b17531615d1ce04f0cce76d703637140b0 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 15 May 2024 13:14:27 +0200 Subject: [PATCH 12/36] Move test class for AuthorizationService --- .../AuthorizationServiceTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename test/Altinn.Notifications.Tests/{Notifications => Notifications.Integrations}/AuthorizationServiceTests.cs (97%) diff --git a/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs similarity index 97% rename from test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs rename to test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs index 805d0b89..aedf0865 100644 --- a/test/Altinn.Notifications.Tests/Notifications/AuthorizationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; - using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; using Altinn.Notifications.Integrations.Authorization; @@ -12,7 +11,7 @@ using Xunit; -namespace Altinn.Notifications.Tests.Notifications; +namespace Altinn.Notifications.Tests.Notifications.Integrations; public class AuthorizationServiceTests { @@ -22,7 +21,7 @@ public class AuthorizationServiceTests public AuthorizationServiceTests() { - _target = new AuthorizationService(_pdpMock.Object); + _target = new AuthorizationService(_pdpMock.Object); } [Fact] From 4aba2dd5bf7c7a7fd3203b0c02b98db65e68a0ff Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 15 May 2024 13:25:22 +0200 Subject: [PATCH 13/36] Rename AuthorizationService to client --- .../{AuthorizationService.cs => AuthorizationClient.cs} | 8 +++++--- src/Altinn.Notifications/Program.cs | 2 +- ...zationServiceTests.cs => AuthorizationClientTests.cs} | 9 +++++---- 3 files changed, 11 insertions(+), 8 deletions(-) rename src/Altinn.Notifications.Integrations/Authorization/{AuthorizationService.cs => AuthorizationClient.cs} (97%) rename test/Altinn.Notifications.Tests/Notifications.Integrations/{AuthorizationServiceTests.cs => AuthorizationClientTests.cs} (95%) diff --git a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs similarity index 97% rename from src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs rename to src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs index 6b34b52d..3c69d078 100644 --- a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs @@ -1,9 +1,11 @@ using System.Security.Claims; + using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Constants; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; using Altinn.Notifications.Core.Integrations; + using static Altinn.Authorization.ABAC.Constants.XacmlConstants; namespace Altinn.Notifications.Integrations.Authorization; @@ -12,7 +14,7 @@ namespace Altinn.Notifications.Integrations.Authorization; /// An implementation of able to check that a potential /// recipient of a notification can access the resource that the notification is about. /// -public class AuthorizationService : IAuthorizationService +public class AuthorizationClient : IAuthorizationService { private const string UserIdUrn = "urn:altinn:userid"; @@ -24,9 +26,9 @@ public class AuthorizationService : IAuthorizationService private readonly IPDP _pdp; /// - /// Initialize a new instance the class with the given dependenices. + /// Initialize a new instance the class with the given dependenices. /// - public AuthorizationService(IPDP pdp) + public AuthorizationClient(IPDP pdp) { _pdp = pdp; } diff --git a/src/Altinn.Notifications/Program.cs b/src/Altinn.Notifications/Program.cs index 94f256a6..40cbb4b4 100644 --- a/src/Altinn.Notifications/Program.cs +++ b/src/Altinn.Notifications/Program.cs @@ -189,7 +189,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.Configure(config.GetSection("PlatformSettings")); services.AddHttpClient(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddKafkaServices(config); services.AddAltinnClients(config); diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs similarity index 95% rename from test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs rename to test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs index aedf0865..ad81d4e2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; + using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; using Altinn.Notifications.Integrations.Authorization; @@ -13,15 +14,15 @@ namespace Altinn.Notifications.Tests.Notifications.Integrations; -public class AuthorizationServiceTests +public class AuthorizationClientTests { private Mock _pdpMock = new Mock(); - private AuthorizationService _target; + private AuthorizationClient _target; - public AuthorizationServiceTests() + public AuthorizationClientTests() { - _target = new AuthorizationService(_pdpMock.Object); + _target = new AuthorizationClient(_pdpMock.Object); } [Fact] From 62b00d9b1959bbb6ed88793a13afa288164d32b3 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 15 May 2024 16:12:59 +0200 Subject: [PATCH 14/36] Change input type --- .../Integrations/IAuthorizationService.cs | 9 ++- .../Authorization/AuthorizationClient.cs | 12 ++-- .../AuthorizationClientTests.cs | 61 +++++++++++++------ 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs index fff67f99..1b75ea2a 100644 --- a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs +++ b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs @@ -1,4 +1,6 @@ -namespace Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Core.Models.ContactPoints; + +namespace Altinn.Notifications.Core.Integrations; /// /// Describes the necessary functions of an authorization service that can perform @@ -10,8 +12,9 @@ public interface IAuthorizationService /// Describes a method that can create an authorization request to authorize a set of /// users for access to a resource. /// - /// The list organizations with associated right holders. + /// The list organizations with associated right holders. /// The id of the resource. /// A task - Task>> AuthorizeUsersForResource(Dictionary> orgRightHolders, string resourceId); + Task>> AuthorizeUsersForResource( + List organizationContactPoints, string resourceId); } diff --git a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs index 3c69d078..ce97f1ca 100644 --- a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs @@ -5,7 +5,7 @@ using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; using Altinn.Notifications.Core.Integrations; - +using Altinn.Notifications.Core.Models.ContactPoints; using static Altinn.Authorization.ABAC.Constants.XacmlConstants; namespace Altinn.Notifications.Integrations.Authorization; @@ -37,10 +37,10 @@ public AuthorizationClient(IPDP pdp) /// An implementation of that /// will generate an authorization call to Altinn Authorization to check that the given users have read access. /// - /// The list organizations with associated right holders. + /// The list organizations with associated right holders. /// The id of the resource. /// A task - public async Task>> AuthorizeUsersForResource(Dictionary> orgRightHolders, string resourceId) + public async Task>> AuthorizeUsersForResource(List organizationContactPoints, string resourceId) { XacmlJsonRequest request = new() { @@ -50,16 +50,16 @@ public async Task>> AuthorizeUsersFo MultiRequests = new XacmlJsonMultiRequests { RequestReference = [] } }; - foreach (var organization in orgRightHolders) + foreach (var organization in organizationContactPoints) { - XacmlJsonCategory resourceCategory = CreateResourceCategory(organization.Key, resourceId); + XacmlJsonCategory resourceCategory = CreateResourceCategory(organization.PartyId, resourceId); if (request.Resource.All(rc => rc.Id != resourceCategory.Id)) { request.Resource.Add(resourceCategory); } - foreach (int userId in organization.Value.Distinct()) + foreach (int userId in organization.UserContactPoints.Select(u => u.UserId).Distinct()) { XacmlJsonCategory subjectCategory = CreateAccessSubjectCategory(userId); diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs index ad81d4e2..9fd9dfd2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs @@ -3,6 +3,7 @@ using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; +using Altinn.Notifications.Core.Models.ContactPoints; using Altinn.Notifications.Integrations.Authorization; using Altinn.Notifications.Tests.TestData; @@ -29,11 +30,19 @@ public AuthorizationClientTests() public async Task AuthorizeUsersForResource_PermitAll() { // Arrange - Dictionary> input = new() - { - { 51326783, new List { 20020164 } }, - { 51529389, new List { 20020106, 20020164 } } - }; + List organizationContactPoints = + [ + new OrganizationContactPoints + { + PartyId = 51326783, + UserContactPoints = [new() { UserId = 20020164 }] + }, + new OrganizationContactPoints + { + PartyId = 51529389, + UserContactPoints = [new() { UserId = 20020106 }, new() { UserId = 20020164 }] + } + ]; XacmlJsonRequestRoot? actualRequest = null; _pdpMock.Setup(pdp => pdp.GetDecisionForRequest(It.IsAny())) @@ -42,7 +51,7 @@ public async Task AuthorizeUsersForResource_PermitAll() // Act Dictionary> actualResult = - await _target.AuthorizeUsersForResource(input, "app_ttd_apps-test"); + await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("PermitAll"); @@ -60,11 +69,19 @@ public async Task AuthorizeUsersForResource_PermitAll() public async Task AuthorizeUsersForResource_DenyOne() { // Arrange - Dictionary> input = new() - { - { 51326783, new List { 20020164 } }, - { 51529389, new List { 20020168, 20020164 } } - }; + List organizationContactPoints = + [ + new OrganizationContactPoints + { + PartyId = 51326783, + UserContactPoints = [new() { UserId = 20020164 }] + }, + new OrganizationContactPoints + { + PartyId = 51529389, + UserContactPoints = [new() { UserId = 20020168 }, new() { UserId = 20020164 }] + } + ]; XacmlJsonRequestRoot? actualRequest = null; _pdpMock.Setup(pdp => pdp.GetDecisionForRequest(It.IsAny())) @@ -73,7 +90,7 @@ public async Task AuthorizeUsersForResource_DenyOne() // Act Dictionary> actualResult = - await _target.AuthorizeUsersForResource(input, "app_ttd_apps-test"); + await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyOne"); @@ -91,11 +108,19 @@ public async Task AuthorizeUsersForResource_DenyOne() public async Task AuthorizeUsersForResource_DenyAll() { // Arrange - Dictionary> input = new() - { - { 51326783, new List { 55555555 } }, - { 51529389, new List { 55555555, 66666666 } } - }; + List organizationContactPoints = + [ + new OrganizationContactPoints + { + PartyId = 51326783, + UserContactPoints = [new() { UserId = 55555555 }] + }, + new OrganizationContactPoints + { + PartyId = 51529389, + UserContactPoints = [new() { UserId = 66666666 }, new() { UserId = 55555555 }] + } + ]; XacmlJsonRequestRoot? actualRequest = null; _pdpMock.Setup(pdp => pdp.GetDecisionForRequest(It.IsAny())) @@ -104,7 +129,7 @@ public async Task AuthorizeUsersForResource_DenyAll() // Act Dictionary> actualResult = - await _target.AuthorizeUsersForResource(input, "app_ttd_apps-test"); + await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyAll"); From 777b7aca2d948fccea64fbef2eb1074ed36bf0b4 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Thu, 16 May 2024 11:47:29 +0200 Subject: [PATCH 15/36] Fix comments and settings --- .../Integrations/IAuthorizationService.cs | 2 +- .../Models/ContactPoints/OrganizationContactPoints.cs | 2 +- src/Altinn.Notifications/appsettings.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs index 1b75ea2a..e6b076b3 100644 --- a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs +++ b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs @@ -12,7 +12,7 @@ public interface IAuthorizationService /// Describes a method that can create an authorization request to authorize a set of /// users for access to a resource. /// - /// The list organizations with associated right holders. + /// The contact points of an organization including user registered contact points. /// The id of the resource. /// A task Task>> AuthorizeUsersForResource( diff --git a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs index 45ebde36..0a3dd5b7 100644 --- a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs +++ b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs @@ -26,7 +26,7 @@ public class OrganizationContactPoints public List EmailList { get; set; } = []; /// - /// Gets or sets a list of user registered contanct points associated with the organisation. + /// Gets or sets a list of user registered contact points associated with the organization. /// public List UserContactPoints { get; set; } = []; } diff --git a/src/Altinn.Notifications/appsettings.json b/src/Altinn.Notifications/appsettings.json index 749ab40d..fd663c66 100644 --- a/src/Altinn.Notifications/appsettings.json +++ b/src/Altinn.Notifications/appsettings.json @@ -2,7 +2,7 @@ "PlatformSettings": { "ApiProfileEndpoint": "http://localhost:5030/profile/api/v1/", "ApiRegisterEndpoint": "http://localhost:5020/register/api/v1/", - "ApiAuthorizationEndpoint": "http://localhost:5030/authorization/api/v1/" + "ApiAuthorizationEndpoint": "http://localhost:5050/authorization/api/v1/" }, "PostgreSQLSettings": { "MigrationScriptPath": "Migration", From 35240e817003df3498964acbaed822c4d4c6f1c9 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Thu, 16 May 2024 16:17:36 +0200 Subject: [PATCH 16/36] Move setup, rename again --- ...ationClient.cs => AuthorizationService.cs} | 6 +++--- .../Extensions/ServiceCollectionExtensions.cs | 19 ++++++++++++++++++- src/Altinn.Notifications/Program.cs | 7 +------ ...tTests.cs => AuthorizationServiceTests.cs} | 8 ++++---- 4 files changed, 26 insertions(+), 14 deletions(-) rename src/Altinn.Notifications.Integrations/Authorization/{AuthorizationClient.cs => AuthorizationService.cs} (97%) rename test/Altinn.Notifications.Tests/Notifications.Integrations/{AuthorizationClientTests.cs => AuthorizationServiceTests.cs} (96%) diff --git a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs similarity index 97% rename from src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs rename to src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs index ce97f1ca..760d9e21 100644 --- a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationClient.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs @@ -14,7 +14,7 @@ namespace Altinn.Notifications.Integrations.Authorization; /// An implementation of able to check that a potential /// recipient of a notification can access the resource that the notification is about. /// -public class AuthorizationClient : IAuthorizationService +public class AuthorizationService : IAuthorizationService { private const string UserIdUrn = "urn:altinn:userid"; @@ -26,9 +26,9 @@ public class AuthorizationClient : IAuthorizationService private readonly IPDP _pdp; /// - /// Initialize a new instance the class with the given dependenices. + /// Initialize a new instance the class with the given dependenices. /// - public AuthorizationClient(IPDP pdp) + public AuthorizationService(IPDP pdp) { _pdp = pdp; } diff --git a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs index f478642c..41952e79 100644 --- a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,8 @@ -using Altinn.Notifications.Core.Integrations; +using Altinn.Common.PEP.Clients; +using Altinn.Common.PEP.Implementation; +using Altinn.Common.PEP.Interfaces; +using Altinn.Notifications.Core.Integrations; +using Altinn.Notifications.Integrations.Authorization; using Altinn.Notifications.Integrations.Clients; using Altinn.Notifications.Integrations.Configuration; using Altinn.Notifications.Integrations.Health; @@ -53,6 +57,19 @@ public static void AddAltinnClients(this IServiceCollection services, IConfigura services.AddHttpClient(); } + /// + /// Adds Altinn clients and configurations to DI container. + /// + /// service collection. + /// the configuration collection + public static void AddAuthorizationService(this IServiceCollection services, IConfiguration config) + { + services.Configure(config.GetSection("PlatformSettings")); + services.AddHttpClient(); + services.AddSingleton(); + services.AddSingleton(); + } + /// /// Adds kafka health checks /// diff --git a/src/Altinn.Notifications/Program.cs b/src/Altinn.Notifications/Program.cs index 40cbb4b4..aa77fd6d 100644 --- a/src/Altinn.Notifications/Program.cs +++ b/src/Altinn.Notifications/Program.cs @@ -185,12 +185,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) ResourceLinkExtensions.Initialize(generalSettings.BaseUri); AddInputModelValidators(services); services.AddCoreServices(config); - - services.Configure(config.GetSection("PlatformSettings")); - services.AddHttpClient(); - services.AddSingleton(); - services.AddSingleton(); - + services.AddAuthorizationService(config); services.AddKafkaServices(config); services.AddAltinnClients(config); services.AddPostgresRepositories(config); diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs similarity index 96% rename from test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs rename to test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs index 9fd9dfd2..b9f7ccbc 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs @@ -15,15 +15,15 @@ namespace Altinn.Notifications.Tests.Notifications.Integrations; -public class AuthorizationClientTests +public class AuthorizationServiceTests { private Mock _pdpMock = new Mock(); - private AuthorizationClient _target; + private AuthorizationService _target; - public AuthorizationClientTests() + public AuthorizationServiceTests() { - _target = new AuthorizationClient(_pdpMock.Object); + _target = new AuthorizationService(_pdpMock.Object); } [Fact] From 1b0bb72fbcd8dcc174d01b7599ba1e9e5be9425b Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Tue, 21 May 2024 15:38:52 +0200 Subject: [PATCH 17/36] Change Authorization return type --- .../Integrations/IAuthorizationService.cs | 2 +- .../OrganizationContactPoints.cs | 25 ++++- .../Models/ContactPoints/UserContactPoints.cs | 16 +++ .../Authorization/AuthorizationService.cs | 105 +++++++++--------- .../AuthorizationServiceTests.cs | 37 +++--- .../TestData/XacmlJsonResponse/DenyAll.json | 6 +- 6 files changed, 121 insertions(+), 70 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs index e6b076b3..0745057d 100644 --- a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs +++ b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs @@ -15,6 +15,6 @@ public interface IAuthorizationService /// The contact points of an organization including user registered contact points. /// The id of the resource. /// A task - Task>> AuthorizeUsersForResource( + Task> AuthorizeUsersForResource( List organizationContactPoints, string resourceId); } diff --git a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs index 0a3dd5b7..8c7ad0d4 100644 --- a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs +++ b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs @@ -1,4 +1,6 @@ -namespace Altinn.Notifications.Core.Models.ContactPoints; +using System.Linq; + +namespace Altinn.Notifications.Core.Models.ContactPoints; /// /// Class describing the contact points for an organization @@ -29,4 +31,25 @@ public class OrganizationContactPoints /// Gets or sets a list of user registered contact points associated with the organization. /// public List UserContactPoints { get; set; } = []; + + /// + /// Create a new instance with the same values as the existing instance + /// + /// The new instance with copied values. + public OrganizationContactPoints CloneWithoutUsers() + { + OrganizationContactPoints clone = new() + { + OrganizationNumber = OrganizationNumber, + PartyId = PartyId, + MobileNumberList = [], + EmailList = [], + UserContactPoints = [] + }; + + clone.MobileNumberList.AddRange(MobileNumberList); + clone.EmailList.AddRange(EmailList); + + return clone; + } } diff --git a/src/Altinn.Notifications.Core/Models/ContactPoints/UserContactPoints.cs b/src/Altinn.Notifications.Core/Models/ContactPoints/UserContactPoints.cs index 0ed6923b..38b058da 100644 --- a/src/Altinn.Notifications.Core/Models/ContactPoints/UserContactPoints.cs +++ b/src/Altinn.Notifications.Core/Models/ContactPoints/UserContactPoints.cs @@ -29,4 +29,20 @@ public class UserContactPoints /// Gets or sets the email address /// public string Email { get; set; } = string.Empty; + + /// + /// Create a new instance with the same values as the existing instance + /// + /// The new instance with copied values. + public UserContactPoints Clone() + { + return new() + { + UserId = UserId, + NationalIdentityNumber = NationalIdentityNumber, + IsReserved = IsReserved, + MobileNumber = MobileNumber, + Email = Email + }; + } } diff --git a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs index 760d9e21..7003804e 100644 --- a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs @@ -40,7 +40,46 @@ public AuthorizationService(IPDP pdp) /// The list organizations with associated right holders. /// The id of the resource. /// A task - public async Task>> AuthorizeUsersForResource(List organizationContactPoints, string resourceId) + public async Task> AuthorizeUsersForResource( + List organizationContactPoints, string resourceId) + { + XacmlJsonRequestRoot jsonRequest = BuildAuthorizationRequest(organizationContactPoints, resourceId); + + XacmlJsonResponse xacmlJsonResponse = await _pdp.GetDecisionForRequest(jsonRequest); + + List filtered = + organizationContactPoints.Select(o => o.CloneWithoutUsers()).ToList(); + + foreach (var response in xacmlJsonResponse.Response.Where(r => r.Decision == "Permit")) + { + string? partyId = GetValue(response, MatchAttributeCategory.Resource, AltinnXacmlUrns.PartyId); + string? userId = GetValue(response, MatchAttributeCategory.Subject, UserIdUrn); + + if (partyId == null || userId == null) + { + continue; + } + + OrganizationContactPoints? sourceOrg = organizationContactPoints.Find(o => o.PartyId == int.Parse(partyId)); + OrganizationContactPoints? targetOrg = filtered.Find(o => o.PartyId == int.Parse(partyId)); + + if (sourceOrg is null || targetOrg is null) + { + continue; + } + + UserContactPoints? user = sourceOrg.UserContactPoints.Find(u => u.UserId == int.Parse(userId)); + + if (user is not null) + { + targetOrg.UserContactPoints.Add(user.Clone()); + } + } + + return filtered; + } + + private XacmlJsonRequestRoot BuildAuthorizationRequest(List organizationContactPoints, string resourceId) { XacmlJsonRequest request = new() { @@ -73,52 +112,7 @@ public async Task>> AuthorizeUsersFo } XacmlJsonRequestRoot jsonRequest = new() { Request = request }; - - XacmlJsonResponse xacmlJsonResponse = await _pdp.GetDecisionForRequest(jsonRequest); - - Dictionary> permit = []; - - foreach (var response in xacmlJsonResponse.Response.Where(r => r.Decision == "Permit")) - { - XacmlJsonCategory? resourceCategory = - response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Resource); - - string? partyId = null; - - if (resourceCategory is not null) - { - XacmlJsonAttribute? partyAttribute = - resourceCategory.Attribute.Find(a => a.AttributeId == AltinnXacmlUrns.PartyId); - - if (partyAttribute is not null) - { - partyId = partyAttribute.Value; - } - } - - XacmlJsonCategory? subjectCategory = - response.Category.Find(c => c.CategoryId == MatchAttributeCategory.Subject); - - if (subjectCategory is not null) - { - XacmlJsonAttribute? userAttribute - = subjectCategory.Attribute.Find(a => a.AttributeId == UserIdUrn); - - if (userAttribute is not null && partyId is not null) - { - if (permit.TryGetValue(partyId, out Dictionary? value)) - { - value.Add(userAttribute.Value, true); - } - else - { - permit.Add(partyId, new Dictionary { { userAttribute.Value, true } }); - } - } - } - } - - return permit; + return jsonRequest; } private XacmlJsonCategory CreateActionCategory() @@ -189,12 +183,23 @@ private static XacmlJsonRequestReference CreateRequestReference(string resourceC { return new XacmlJsonRequestReference { - ReferenceId = new List - { + ReferenceId = + [ subjectCategoryId, ActionCategoryId, resourceCategoryId - } + ] }; } + + private static string? GetValue(XacmlJsonResult response, string categoryId, string attributeId) + { + XacmlJsonCategory? resourceCategory = + response.Category.Find(c => c.CategoryId == categoryId); + + XacmlJsonAttribute? partyAttribute = + resourceCategory?.Attribute.Find(a => a.AttributeId == attributeId); + + return partyAttribute?.Value; + } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs index b9f7ccbc..bb197493 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs @@ -50,19 +50,14 @@ public async Task AuthorizeUsersForResource_PermitAll() .ReturnsAsync(await TestDataLoader.Load("PermitAll")); // Act - Dictionary> actualResult = + List actualResult = await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("PermitAll"); actualRequest.Should().BeEquivalentTo(expectedRequest); - Dictionary> expectedResult = new() - { - { "51326783", new Dictionary() { { "20020164", true } } }, - { "51529389", new Dictionary() { { "20020106", true }, { "20020164", true } } } - }; - actualResult.Should().BeEquivalentTo(expectedResult); + actualResult.Should().BeEquivalentTo(organizationContactPoints); } [Fact] @@ -89,18 +84,26 @@ public async Task AuthorizeUsersForResource_DenyOne() .ReturnsAsync(await TestDataLoader.Load("DenyOne")); // Act - Dictionary> actualResult = + List actualResult = await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyOne"); actualRequest.Should().BeEquivalentTo(expectedRequest); - Dictionary> expectedResult = new() - { - { "51326783", new Dictionary() { { "20020164", true } } }, - { "51529389", new Dictionary() { { "20020164", true } } } - }; + List expectedResult = + [ + new OrganizationContactPoints + { + PartyId = 51326783, + UserContactPoints = [new() { UserId = 20020164 }] + }, + new OrganizationContactPoints + { + PartyId = 51529389, + UserContactPoints = [new() { UserId = 20020164 }] + } + ]; actualResult.Should().BeEquivalentTo(expectedResult); } @@ -128,14 +131,18 @@ public async Task AuthorizeUsersForResource_DenyAll() .ReturnsAsync(await TestDataLoader.Load("DenyAll")); // Act - Dictionary> actualResult = + List actualResult = await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyAll"); actualRequest.Should().BeEquivalentTo(expectedRequest); - Dictionary> expectedResult = []; // Empty + List expectedResult = + [ + new OrganizationContactPoints { PartyId = 51326783, UserContactPoints = [] }, + new OrganizationContactPoints { PartyId = 51529389, UserContactPoints = [] } + ]; actualResult.Should().BeEquivalentTo(expectedResult); } } diff --git a/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json index 409700a1..e2c6e03e 100644 --- a/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json +++ b/test/Altinn.Notifications.Tests/TestData/XacmlJsonResponse/DenyAll.json @@ -26,7 +26,7 @@ "attribute": [ { "attributeId": "urn:altinn:userid", - "value": "20020164", + "value": "55555555", "dataType": "http://www.w3.org/2001/XMLSchema#string", "includeInResult": false } @@ -58,7 +58,7 @@ "attribute": [ { "attributeId": "urn:altinn:userid", - "value": "20020168", + "value": "66666666", "dataType": "http://www.w3.org/2001/XMLSchema#string", "includeInResult": false } @@ -103,7 +103,7 @@ "attribute": [ { "attributeId": "urn:altinn:userid", - "value": "20020164", + "value": "55555555", "dataType": "http://www.w3.org/2001/XMLSchema#string", "includeInResult": false } From c8333dd7c88b98d2e0faa89523df1de8b596bb48 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 22 May 2024 11:02:00 +0200 Subject: [PATCH 18/36] Fixes based on feedback --- .../Integrations/IAuthorizationService.cs | 8 +++++--- .../Authorization/AuthorizationService.cs | 6 +++--- .../Extensions/ServiceCollectionExtensions.cs | 6 +++--- src/Altinn.Notifications/Program.cs | 4 ---- .../AuthorizationServiceTests.cs | 6 +++--- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs index 0745057d..a299b3cf 100644 --- a/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs +++ b/src/Altinn.Notifications.Core/Integrations/IAuthorizationService.cs @@ -12,9 +12,11 @@ public interface IAuthorizationService /// Describes a method that can create an authorization request to authorize a set of /// users for access to a resource. /// - /// The contact points of an organization including user registered contact points. + /// + /// The contact points of an organization including user registered contact points. + /// /// The id of the resource. - /// A task - Task> AuthorizeUsersForResource( + /// A new list of with filtered list of recipients. + Task> AuthorizeUserContactPointsForResource( List organizationContactPoints, string resourceId); } diff --git a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs index 7003804e..419576cb 100644 --- a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs @@ -34,13 +34,13 @@ public AuthorizationService(IPDP pdp) } /// - /// An implementation of that + /// An implementation of that /// will generate an authorization call to Altinn Authorization to check that the given users have read access. /// /// The list organizations with associated right holders. /// The id of the resource. - /// A task - public async Task> AuthorizeUsersForResource( + /// A new list of with filtered list of recipients. + public async Task> AuthorizeUserContactPointsForResource( List organizationContactPoints, string resourceId) { XacmlJsonRequestRoot jsonRequest = BuildAuthorizationRequest(organizationContactPoints, resourceId); diff --git a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs index 41952e79..31bc1b3c 100644 --- a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs @@ -58,10 +58,10 @@ public static void AddAltinnClients(this IServiceCollection services, IConfigura } /// - /// Adds Altinn clients and configurations to DI container. + /// Adds services and other dependencies used for authorization. /// - /// service collection. - /// the configuration collection + /// The service collection. + /// The configuration collection public static void AddAuthorizationService(this IServiceCollection services, IConfiguration config) { services.Configure(config.GetSection("PlatformSettings")); diff --git a/src/Altinn.Notifications/Program.cs b/src/Altinn.Notifications/Program.cs index aa77fd6d..e75ab443 100644 --- a/src/Altinn.Notifications/Program.cs +++ b/src/Altinn.Notifications/Program.cs @@ -6,15 +6,11 @@ using Altinn.Common.AccessToken; using Altinn.Common.AccessToken.Services; using Altinn.Common.PEP.Authorization; -using Altinn.Common.PEP.Clients; -using Altinn.Common.PEP.Implementation; -using Altinn.Common.PEP.Interfaces; using Altinn.Notifications.Authorization; using Altinn.Notifications.Configuration; using Altinn.Notifications.Core.Extensions; using Altinn.Notifications.Extensions; using Altinn.Notifications.Health; -using Altinn.Notifications.Integrations.Authorization; using Altinn.Notifications.Integrations.Extensions; using Altinn.Notifications.Middleware; using Altinn.Notifications.Models; diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs index bb197493..b5d72f4e 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs @@ -51,7 +51,7 @@ public async Task AuthorizeUsersForResource_PermitAll() // Act List actualResult = - await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); + await _target.AuthorizeUserContactPointsForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("PermitAll"); @@ -85,7 +85,7 @@ public async Task AuthorizeUsersForResource_DenyOne() // Act List actualResult = - await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); + await _target.AuthorizeUserContactPointsForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyOne"); @@ -132,7 +132,7 @@ public async Task AuthorizeUsersForResource_DenyAll() // Act List actualResult = - await _target.AuthorizeUsersForResource(organizationContactPoints, "app_ttd_apps-test"); + await _target.AuthorizeUserContactPointsForResource(organizationContactPoints, "app_ttd_apps-test"); // Assert XacmlJsonRequestRoot expectedRequest = await TestDataLoader.Load("DenyAll"); From 8f732b1799803dec19cccafefd4c6eb3ccd020e0 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 22 May 2024 11:12:10 +0200 Subject: [PATCH 19/36] Move test class AuthorizationServiceTests --- .../AuthorizationServiceTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename test/Altinn.Notifications.Tests/Notifications.Integrations/{ => Authorization}/AuthorizationServiceTests.cs (95%) diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Authorization/AuthorizationServiceTests.cs similarity index 95% rename from test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs rename to test/Altinn.Notifications.Tests/Notifications.Integrations/Authorization/AuthorizationServiceTests.cs index b5d72f4e..338d1789 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/AuthorizationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Authorization/AuthorizationServiceTests.cs @@ -13,7 +13,7 @@ using Xunit; -namespace Altinn.Notifications.Tests.Notifications.Integrations; +namespace Altinn.Notifications.Tests.Notifications.Integrations.Authorization; public class AuthorizationServiceTests { @@ -32,14 +32,14 @@ public async Task AuthorizeUsersForResource_PermitAll() // Arrange List organizationContactPoints = [ - new OrganizationContactPoints - { - PartyId = 51326783, + new OrganizationContactPoints + { + PartyId = 51326783, UserContactPoints = [new() { UserId = 20020164 }] }, - new OrganizationContactPoints - { - PartyId = 51529389, + new OrganizationContactPoints + { + PartyId = 51529389, UserContactPoints = [new() { UserId = 20020106 }, new() { UserId = 20020164 }] } ]; From a285c69e7d610d62e1017dfaa0c5f7c1b589a9af Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Wed, 22 May 2024 11:32:39 +0200 Subject: [PATCH 20/36] Change clone of OrganizationContactPoints --- .../Models/ContactPoints/OrganizationContactPoints.cs | 5 +---- .../Authorization/AuthorizationService.cs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs index 8c7ad0d4..73c80357 100644 --- a/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs +++ b/src/Altinn.Notifications.Core/Models/ContactPoints/OrganizationContactPoints.cs @@ -36,7 +36,7 @@ public class OrganizationContactPoints /// Create a new instance with the same values as the existing instance /// /// The new instance with copied values. - public OrganizationContactPoints CloneWithoutUsers() + public OrganizationContactPoints CloneWithoutContactPoints() { OrganizationContactPoints clone = new() { @@ -46,9 +46,6 @@ public OrganizationContactPoints CloneWithoutUsers() EmailList = [], UserContactPoints = [] }; - - clone.MobileNumberList.AddRange(MobileNumberList); - clone.EmailList.AddRange(EmailList); return clone; } diff --git a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs index 419576cb..4dc7a6b9 100644 --- a/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs +++ b/src/Altinn.Notifications.Integrations/Authorization/AuthorizationService.cs @@ -48,7 +48,7 @@ public async Task> AuthorizeUserContactPointsFor XacmlJsonResponse xacmlJsonResponse = await _pdp.GetDecisionForRequest(jsonRequest); List filtered = - organizationContactPoints.Select(o => o.CloneWithoutUsers()).ToList(); + organizationContactPoints.Select(o => o.CloneWithoutContactPoints()).ToList(); foreach (var response in xacmlJsonResponse.Response.Where(r => r.Decision == "Permit")) { From 05dac523bfa7398f8de8842f1403e21170d13cb9 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 22 May 2024 12:10:36 +0200 Subject: [PATCH 21/36] current progress --- .../Extensions/ServiceCollectionExtensions.cs | 1 - .../Utils/ServiceUtil.cs | 4 +- .../Profile/ProfileClientTests.cs | 68 +++++++++++++++++-- .../Register/RegisterClientTests.cs | 4 +- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs index 6a59cbbd..dbde0972 100644 --- a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs @@ -66,7 +66,6 @@ public static void AddAltinnClients(this IServiceCollection services, IConfigura public static void AddAuthorizationService(this IServiceCollection services, IConfiguration config) { services.Configure(config.GetSection("PlatformSettings")); - services.AddSingleton(); services.AddHttpClient(); services.AddSingleton(); services.AddSingleton(); diff --git a/test/Altinn.Notifications.IntegrationTests/Utils/ServiceUtil.cs b/test/Altinn.Notifications.IntegrationTests/Utils/ServiceUtil.cs index 6d4afae3..b1a110b7 100644 --- a/test/Altinn.Notifications.IntegrationTests/Utils/ServiceUtil.cs +++ b/test/Altinn.Notifications.IntegrationTests/Utils/ServiceUtil.cs @@ -3,6 +3,7 @@ using Altinn.Notifications.Persistence.Extensions; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -33,12 +34,13 @@ public static List GetServices(List interfaceTypes, Dictionary(); services.AddLogging(); services.AddPostgresRepositories(config); - services.AddAuthorizationService(config); services.AddCoreServices(config); services.AddKafkaServices(config); services.AddAltinnClients(config); + services.AddAuthorizationService(config); var serviceProvider = services.BuildServiceProvider(); List outputServices = new(); diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs index 19959ab6..4468cb08 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs @@ -30,12 +30,17 @@ public class ProfileClientTests public ProfileClientTests() { - var sblBridgeHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => + var profileHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => { - if (request!.RequestUri!.AbsolutePath.EndsWith("contactpoint/lookup")) + if (request!.RequestUri!.AbsolutePath.EndsWith("users/contactpoint/lookup")) { UserContactPointLookup? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), JsonSerializerOptionsProvider.Options); - return await GetResponse(lookup!); + return await GetUserProfileResponse(lookup!); + } + else if (request!.RequestUri!.AbsolutePath.EndsWith("units/contactpoint/lookup")) + { + UnitContactPointLookup? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), JsonSerializerOptionsProvider.Options); + return await GetUnitProfileResponse(lookup!); } return new HttpResponseMessage(HttpStatusCode.NotFound); @@ -47,7 +52,7 @@ public ProfileClientTests() }; _profileClient = new ProfileClient( - new HttpClient(sblBridgeHttpMessageHandler), + new HttpClient(profileHttpMessageHandler), Options.Create(settings)); } @@ -82,7 +87,28 @@ public async Task GetUserContactPoints_FailureResponse_ExceptionIsThrown() Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } - private Task GetResponse(UserContactPointLookup lookup) + [Fact] + public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_NoMatches() + { + // Act + List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints("no-matches", ["12345678", "98754321"]); + + // Assert + Assert.Empty(actual); + } + + [Fact] + public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_TwoListElementsReturned() + { + // Act + List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints("some-matches", ["12345678", "98754321"]); + + // Assert + Assert.Equal(2, actual.Count); + Assert.Contains(new OrganizationContactPoints() { OrganizationNumber = "123456789", PartyId = 56789, UserContactPoints = [new() { UserId = 20001 }] }, actual); + } + + private Task GetUserProfileResponse(UserContactPointLookup lookup) { HttpStatusCode statusCode = HttpStatusCode.OK; object? contentData = null; @@ -116,4 +142,36 @@ private Task GetResponse(UserContactPointLookup lookup) Content = content }); } + + private Task GetUnitProfileResponse(UnitContactPointLookup lookup) + { + HttpStatusCode statusCode = HttpStatusCode.OK; + object? contentData = null; + + switch (lookup.ResourceId) + { + case "no-matches": + contentData = new List(); + break; + case "some-matches": + contentData = new List + { + new() { OrganizationNumber = "123456789", PartyId = 56789, UserContactPoints = [new() { UserId = 20001 }] }, + new() { OrganizationNumber = "987654321", PartyId = 54321, UserContactPoints = [new() { UserId = 20001 }] } + }; + break; + case "error-resource": + statusCode = HttpStatusCode.ServiceUnavailable; + break; + } + + JsonContent? content = (contentData != null) ? JsonContent.Create(contentData, options: _serializerOptions) : null; + + return Task.FromResult( + new HttpResponseMessage() + { + StatusCode = statusCode, + Content = content + }); + } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs index 30b803b5..154f9f4b 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Register/RegisterClientTests.cs @@ -29,7 +29,7 @@ public class RegisterClientTests public RegisterClientTests() { - var sblBridgeHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => + var registerHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => { if (request!.RequestUri!.AbsolutePath.EndsWith("contactpoint/lookup")) { @@ -46,7 +46,7 @@ public RegisterClientTests() }; _registerClient = new RegisterClient( - new HttpClient(sblBridgeHttpMessageHandler), + new HttpClient(registerHttpMessageHandler), Options.Create(settings)); } From e1db20eaec935a51198a266b69913c290830ea16 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 22 May 2024 12:33:09 +0200 Subject: [PATCH 22/36] tests running for profile client --- .../Services/ContactPointService.cs | 2 +- .../Profile/ProfileClient.cs | 2 +- .../Profile/ProfileClientTests.cs | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index 9f45f6e0..5a312cc1 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -167,7 +167,7 @@ private async Task> LookupOrganizationContactPoi if (!string.IsNullOrEmpty(resourceId)) { var allUserContactPoints = await _profileClient.GetUserRegisteredOrganizationContactPoints(resourceId, orgNos); - authorizedUserContactPoints = await _authorizationService.AuthorizeUsersForResource(allUserContactPoints, resourceId); + authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); } List contactPoints = await registerTask; diff --git a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs index 9a55ad28..f49e0885 100644 --- a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs +++ b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs @@ -69,7 +69,7 @@ public async Task> GetUserRegisteredOrganization } string responseContent = await response.Content.ReadAsStringAsync(); - List? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; + List? contactPoints = JsonSerializer.Deserialize>(responseContent, JsonSerializerOptionsProvider.Options)!; return contactPoints!; } diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs index 4468cb08..5e29b4b0 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs @@ -105,7 +105,17 @@ public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_Two // Assert Assert.Equal(2, actual.Count); - Assert.Contains(new OrganizationContactPoints() { OrganizationNumber = "123456789", PartyId = 56789, UserContactPoints = [new() { UserId = 20001 }] }, actual); + Assert.Contains(actual, ocp => ocp.OrganizationNumber == "123456789" && ocp.PartyId == 56789 && ocp.UserContactPoints.Any(u => u.UserId == 20001)); + } + + [Fact] + public async Task GetUserRegisteredOrganizationContactPoints_FailureResponse_ExceptionIsThrown() + { + // Act + var exception = await Assert.ThrowsAsync(async () => await _profileClient.GetUserRegisteredOrganizationContactPoints("error-resource", ["12345678", "98754321"])); + + Assert.StartsWith("ProfileClient.GetUserRegisteredOrganizationContactPoints failed with status code", exception.Message); + Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } private Task GetUserProfileResponse(UserContactPointLookup lookup) From 5d010191b6f286e4b52c99e67141766863791e1d Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 22 May 2024 15:10:42 +0200 Subject: [PATCH 23/36] cleaned up k6 file obsolete props --- test/k6/src/api/authentication.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/k6/src/api/authentication.js b/test/k6/src/api/authentication.js index 6361ced7..f3042554 100644 --- a/test/k6/src/api/authentication.js +++ b/test/k6/src/api/authentication.js @@ -2,15 +2,11 @@ import { check } from "k6"; import http from "k6/http"; import { - buildHeaderWithBearer, - buildHeaderWithContentType, - buildHeaderWithCookie, + buildHeaderWithBearer } from "../apiHelpers.js"; -import { platformAuthentication, portalAuthentication } from "../config.js"; +import { platformAuthentication } from "../config.js"; import { stopIterationOnFail, addErrorCount } from "../errorhandler.js"; -const userName = __ENV.userName; -const userPassword = __ENV.userPassword; export function exchangeToAltinnToken(token, test) { var endpoint = platformAuthentication.exchange + "?test=" + test; From 096be108f8ac4f64585739c839a27910ebbfe6e2 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 23 May 2024 11:51:13 +0200 Subject: [PATCH 24/36] added test for authorization result merge --- .../Integrations/IProfileClient.cs | 6 +- .../Services/ContactPointService.cs | 2 +- .../Profile/ProfileClient.cs | 2 +- .../ContactPointServiceTests.cs | 207 ++++++++++++++++++ .../Profile/ProfileClientTests.cs | 7 +- 5 files changed, 216 insertions(+), 8 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs index 28ed22d5..cab0f2df 100644 --- a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs @@ -17,8 +17,8 @@ public interface IProfileClient /// /// Retrieves the user registered contact points for a list of organization corresponding to a list of organization numbers /// - /// The id of the resource to look up contact points for /// The set or organizations to retrieve contact points for - /// - public Task> GetUserRegisteredOrganizationContactPoints(string resourceId, List organizationNumbers); + /// The id of the resource to look up contact points for + /// A list of organiation contact points containing user registered contact points + public Task> GetUserRegisteredOrganizationContactPoints(List organizationNumbers, string resourceId); } diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index 5a312cc1..9b4a9008 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -166,7 +166,7 @@ private async Task> LookupOrganizationContactPoi if (!string.IsNullOrEmpty(resourceId)) { - var allUserContactPoints = await _profileClient.GetUserRegisteredOrganizationContactPoints(resourceId, orgNos); + var allUserContactPoints = await _profileClient.GetUserRegisteredOrganizationContactPoints(orgNos, resourceId); authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); } diff --git a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs index f49e0885..18fc776b 100644 --- a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs +++ b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs @@ -51,7 +51,7 @@ public async Task> GetUserContactPoints(List nat } /// - public async Task> GetUserRegisteredOrganizationContactPoints(string resourceId, List organizationNumbers) + public async Task> GetUserRegisteredOrganizationContactPoints(List organizationNumbers, string resourceId) { var lookupObject = new UnitContactPointLookup() { diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index 7acca0c0..35f8e2d9 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Altinn.Notifications.Core.Integrations; @@ -151,6 +152,212 @@ public async Task AddEmailContactPoints_OrganizationNumberAvailable_RegisterServ Assert.Equivalent(expectedOutput, input); } + [Fact] + public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_AuthorizationPermitAll() + { + // Arrange + string resource = "urn:altinn:resource"; + + List input = [ + new Recipient() + { + OrganizationNumber = "12345678901" + } + ]; + + List expectedOutput = [ + new Recipient() + { + OrganizationNumber = "12345678901", + AddressInfo = [new EmailAddressPoint("official@domain.com"), new EmailAddressPoint("user-1@domain.com"), new EmailAddressPoint("user-9@domain.com")] + } + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["official@domain.com"] }]); + + var profileClientMock = new Mock(); + profileClientMock + .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync([ + new OrganizationContactPoints() + { + PartyId = 78901, + OrganizationNumber = "12345678901", + UserContactPoints = [ + new UserContactPoints() + { + UserId = 200001, + Email = "user-1@domain.com" + }, + new UserContactPoints() + { + UserId = 200009, + Email = "user-9@domain.com" + } + ] + } + ]); + + var authorizationServiceMock = new Mock(); + authorizationServiceMock + .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync((List input, string resource) => input); + + var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); + + // Act + await service.AddEmailContactPoints(input, resource); + + // Assert + registerClientMock.VerifyAll(); + profileClientMock.VerifyAll(); + authorizationServiceMock.VerifyAll(); + Assert.Equivalent(expectedOutput, input); + } + + [Fact] + public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_AuthorizationDenyOne() + { + // Arrange + string resource = "urn:altinn:resource"; + + List input = [ + new Recipient() + { + OrganizationNumber = "12345678901" + } + ]; + + List expectedOutput = [ + new Recipient() + { + OrganizationNumber = "12345678901", + AddressInfo = [new EmailAddressPoint("official@domain.com"), new EmailAddressPoint("user-1@domain.com")] + } + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["official@domain.com"] }]); + + var profileClientMock = new Mock(); + profileClientMock + .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync([ + new OrganizationContactPoints() + { + PartyId = 78901, + OrganizationNumber = "12345678901", + UserContactPoints = [ + new UserContactPoints() + { + UserId = 200001, + Email = "user-1@domain.com" + }, + new UserContactPoints() + { + UserId = 200009, + Email = "user-9@domain.com" + } + ] + } + ]); + + var authorizationServiceMock = new Mock(); + authorizationServiceMock + .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync((List input, string resource) => + { + input[0].UserContactPoints.RemoveAll(u => u.UserId == 200009); + return input; + }); + + var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); + + // Act + await service.AddEmailContactPoints(input, resource); + + // Assert + registerClientMock.VerifyAll(); + profileClientMock.VerifyAll(); + authorizationServiceMock.VerifyAll(); + Assert.Equivalent(expectedOutput, input); + } + + [Fact] + public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_AuthorizationDenyAll() + { + // Arrange + string resource = "urn:altinn:resource"; + + List input = [ + new Recipient() + { + OrganizationNumber = "12345678901" + } + ]; + + List expectedOutput = [ + new Recipient() + { + OrganizationNumber = "12345678901", + AddressInfo = [new EmailAddressPoint("official@domain.com")] + } + ]; + + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["official@domain.com"] }]); + + var profileClientMock = new Mock(); + profileClientMock + .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync([ + new OrganizationContactPoints() + { + PartyId = 78901, + OrganizationNumber = "12345678901", + UserContactPoints = [ + new UserContactPoints() + { + UserId = 200001, + Email = "user-1@domain.com" + }, + new UserContactPoints() + { + UserId = 200009, + Email = "user-9@domain.com" + } + ] + } + ]); + + var authorizationServiceMock = new Mock(); + authorizationServiceMock + .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync((List input, string resource) => + { + input.ForEach(ocp => ocp.UserContactPoints = []); + return input; + }); + + var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); + + // Act + await service.AddEmailContactPoints(input, resource); + + // Assert + registerClientMock.VerifyAll(); + profileClientMock.VerifyAll(); + authorizationServiceMock.VerifyAll(); + Assert.Equivalent(expectedOutput, input); + } + private static ContactPointService GetTestService( IProfileClient? profileClient = null, IRegisterClient? registerClient = null, diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs index 5e29b4b0..a83bb11f 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs @@ -91,7 +91,7 @@ public async Task GetUserContactPoints_FailureResponse_ExceptionIsThrown() public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_NoMatches() { // Act - List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints("no-matches", ["12345678", "98754321"]); + List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints(["12345678", "98754321"], "no-matches"); // Assert Assert.Empty(actual); @@ -101,7 +101,7 @@ public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_NoM public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_TwoListElementsReturned() { // Act - List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints("some-matches", ["12345678", "98754321"]); + List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints(["12345678", "98754321"], "some-matches"); // Assert Assert.Equal(2, actual.Count); @@ -112,7 +112,8 @@ public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_Two public async Task GetUserRegisteredOrganizationContactPoints_FailureResponse_ExceptionIsThrown() { // Act - var exception = await Assert.ThrowsAsync(async () => await _profileClient.GetUserRegisteredOrganizationContactPoints("error-resource", ["12345678", "98754321"])); + var exception = await Assert.ThrowsAsync( + async () => await _profileClient.GetUserRegisteredOrganizationContactPoints(["12345678", "98754321"], "error-resource")); Assert.StartsWith("ProfileClient.GetUserRegisteredOrganizationContactPoints failed with status code", exception.Message); Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); From d33efc6353cd0061e03958bb9bffb6f05b829814 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 23 May 2024 12:19:18 +0200 Subject: [PATCH 25/36] removed obsolete file --- .../Profile/OrganizationContactPointsList.cs | 15 --------------- .../TestingServices/ContactPointServiceTests.cs | 6 +++--- 2 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs diff --git a/src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs b/src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs deleted file mode 100644 index ca6108e1..00000000 --- a/src/Altinn.Notifications.Integrations/Profile/OrganizationContactPointsList.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Altinn.Notifications.Core.Models.ContactPoints; - -namespace Altinn.Notifications.Integrations.Profile -{ - /// - /// A list representation of - /// - public class OrganizationContactPointsList - { - /// - /// A list containing contact points for users - /// - public List ContactPointsList { get; set; } = []; - } -} diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index 35f8e2d9..7f2f4c1d 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -219,7 +219,7 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_A } [Fact] - public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_AuthorizationDenyOne() + public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_NoOfficialContact_AuthorizationDenyOne() { // Arrange string resource = "urn:altinn:resource"; @@ -235,14 +235,14 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_A new Recipient() { OrganizationNumber = "12345678901", - AddressInfo = [new EmailAddressPoint("official@domain.com"), new EmailAddressPoint("user-1@domain.com")] + AddressInfo = [new EmailAddressPoint("user-1@domain.com")] } ]; var registerClientMock = new Mock(); registerClientMock .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) - .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["official@domain.com"] }]); + .ReturnsAsync([]); var profileClientMock = new Mock(); profileClientMock From 6b9aea8cf57978b3a0a6aa31375d12af7e35f0a4 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 23 May 2024 12:47:31 +0200 Subject: [PATCH 26/36] adjustet test to cover sms --- .../ContactPointServiceTests.cs | 126 +++++++++--------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index 7f2f4c1d..b636fb03 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -86,64 +86,99 @@ public async Task AddSmsContactPoints_OrganizationNumberAvailable_RegisterServic } [Fact] - public async Task AddEmailContactPoints_NationalIdentityNumberAvailable_ProfileServiceCalled() + public async Task AddSmsContactPoints_OrganizationNumberAndResourceAvailable_AuthorizationPermitAll() { // Arrange + string resource = "urn:altinn:resource"; + List input = [ new Recipient() { - NationalIdentityNumber = "12345678901" + OrganizationNumber = "12345678901" } ]; List expectedOutput = [ new Recipient() { - NationalIdentityNumber = "12345678901", - IsReserved = true, - AddressInfo = [new EmailAddressPoint("email@domain.com")] + OrganizationNumber = "12345678901", + AddressInfo = [new SmsAddressPoint("+4799999999"), new SmsAddressPoint("+4748123456"), new SmsAddressPoint("+4699999999")] } ]; + var registerClientMock = new Mock(); + registerClientMock + .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) + .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", MobileNumberList = ["+4799999999"] }]); + var profileClientMock = new Mock(); profileClientMock - .Setup(p => p.GetUserContactPoints(It.Is>(s => s.Contains("12345678901")))) - .ReturnsAsync([new UserContactPoints() { NationalIdentityNumber = "12345678901", Email = "email@domain.com", IsReserved = true }]); + .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync([ + new OrganizationContactPoints() + { + PartyId = 78901, + OrganizationNumber = "12345678901", + UserContactPoints = [ + new UserContactPoints() + { + UserId = 200001, + MobileNumber = "+4748123456", + Email = "user-1@domain.com" + }, + new UserContactPoints() + { + UserId = 200009, + MobileNumber = "004699999999", + Email = "user-9@domain.com" + } + ] + } + ]); - var service = GetTestService(profileClient: profileClientMock.Object); + var authorizationServiceMock = new Mock(); + authorizationServiceMock + .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .ReturnsAsync((List input, string resource) => input); + + var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); // Act - await service.AddEmailContactPoints(input, null); + await service.AddSmsContactPoints(input, resource); // Assert + registerClientMock.VerifyAll(); + profileClientMock.VerifyAll(); + authorizationServiceMock.VerifyAll(); Assert.Equivalent(expectedOutput, input); } [Fact] - public async Task AddEmailContactPoints_OrganizationNumberAvailable_RegisterServiceCalled() + public async Task AddEmailContactPoints_NationalIdentityNumberAvailable_ProfileServiceCalled() { // Arrange List input = [ new Recipient() { - OrganizationNumber = "12345678901" + NationalIdentityNumber = "12345678901" } ]; List expectedOutput = [ new Recipient() { - OrganizationNumber = "12345678901", + NationalIdentityNumber = "12345678901", + IsReserved = true, AddressInfo = [new EmailAddressPoint("email@domain.com")] } ]; - var registerClientMock = new Mock(); - registerClientMock - .Setup(p => p.GetOrganizationContactPoints(It.Is>(s => s.Contains("12345678901")))) - .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["email@domain.com"] }]); + var profileClientMock = new Mock(); + profileClientMock + .Setup(p => p.GetUserContactPoints(It.Is>(s => s.Contains("12345678901")))) + .ReturnsAsync([new UserContactPoints() { NationalIdentityNumber = "12345678901", Email = "email@domain.com", IsReserved = true }]); - var service = GetTestService(registerClient: registerClientMock.Object); + var service = GetTestService(profileClient: profileClientMock.Object); // Act await service.AddEmailContactPoints(input, null); @@ -153,11 +188,9 @@ public async Task AddEmailContactPoints_OrganizationNumberAvailable_RegisterServ } [Fact] - public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_AuthorizationPermitAll() + public async Task AddEmailContactPoints_OrganizationNumberAvailable_RegisterServiceCalled() { // Arrange - string resource = "urn:altinn:resource"; - List input = [ new Recipient() { @@ -169,52 +202,21 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_A new Recipient() { OrganizationNumber = "12345678901", - AddressInfo = [new EmailAddressPoint("official@domain.com"), new EmailAddressPoint("user-1@domain.com"), new EmailAddressPoint("user-9@domain.com")] + AddressInfo = [new EmailAddressPoint("email@domain.com")] } ]; var registerClientMock = new Mock(); registerClientMock - .Setup(r => r.GetOrganizationContactPoints(It.IsAny>())) - .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["official@domain.com"] }]); - - var profileClientMock = new Mock(); - profileClientMock - .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) - .ReturnsAsync([ - new OrganizationContactPoints() - { - PartyId = 78901, - OrganizationNumber = "12345678901", - UserContactPoints = [ - new UserContactPoints() - { - UserId = 200001, - Email = "user-1@domain.com" - }, - new UserContactPoints() - { - UserId = 200009, - Email = "user-9@domain.com" - } - ] - } - ]); - - var authorizationServiceMock = new Mock(); - authorizationServiceMock - .Setup(a => a.AuthorizeUserContactPointsForResource(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) - .ReturnsAsync((List input, string resource) => input); + .Setup(p => p.GetOrganizationContactPoints(It.Is>(s => s.Contains("12345678901")))) + .ReturnsAsync([new OrganizationContactPoints() { OrganizationNumber = "12345678901", EmailList = ["email@domain.com"] }]); - var service = GetTestService(profileClientMock.Object, registerClientMock.Object, authorizationServiceMock.Object); + var service = GetTestService(registerClient: registerClientMock.Object); // Act - await service.AddEmailContactPoints(input, resource); + await service.AddEmailContactPoints(input, null); // Assert - registerClientMock.VerifyAll(); - profileClientMock.VerifyAll(); - authorizationServiceMock.VerifyAll(); Assert.Equivalent(expectedOutput, input); } @@ -253,14 +255,16 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_N PartyId = 78901, OrganizationNumber = "12345678901", UserContactPoints = [ - new UserContactPoints() - { - UserId = 200001, - Email = "user-1@domain.com" - }, + new UserContactPoints() + { + UserId = 200001, + MobileNumber = "+4748123456", + Email = "user-1@domain.com" + }, new UserContactPoints() { UserId = 200009, + MobileNumber = "004699999999", Email = "user-9@domain.com" } ] From 3c23e6aa1e91fbcec2e31076c8c6df522ef92565 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 23 May 2024 13:57:28 +0200 Subject: [PATCH 27/36] fixed indentation --- .../TestingServices/ContactPointServiceTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index b636fb03..042c8a64 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -261,13 +261,13 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_N MobileNumber = "+4748123456", Email = "user-1@domain.com" }, - new UserContactPoints() - { - UserId = 200009, - MobileNumber = "004699999999", - Email = "user-9@domain.com" - } - ] + new UserContactPoints() + { + UserId = 200009, + MobileNumber = "004699999999", + Email = "user-9@domain.com" + } + ] } ]); From 5cdb0c60c62b25369ab403888cd1c9af30fe7d2a Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 23 May 2024 17:10:16 +0200 Subject: [PATCH 28/36] Fixed errors in code --- src/Altinn.Notifications.Core/Services/ContactPointService.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index 9b4a9008..fbc2f5f8 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -149,7 +149,7 @@ private async Task> LookupPersonContactPoints(List> LookupOrganizationContactPoints(List recipients, string? resourceId = null) + private async Task> LookupOrganizationContactPoints(List recipients, string? resourceId) { List orgNos = recipients .Where(r => !string.IsNullOrEmpty(r.OrganizationNumber)) diff --git a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs index 9fb04241..31bc1b3c 100644 --- a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ using Altinn.Notifications.Integrations.Kafka.Producers; using Altinn.Notifications.Integrations.Register; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; From cc325a711e6bc11d087b2025342236d816ad71a4 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 23 May 2024 18:00:34 +0200 Subject: [PATCH 29/36] fixed return type for profile api response --- .../Profile/ProfileClient.cs | 5 +++-- .../Profile/ProfileClientTests.cs | 12 ++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs index 18fc776b..ccea1fe3 100644 --- a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs +++ b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs @@ -7,6 +7,7 @@ using Altinn.Notifications.Core.Shared; using Altinn.Notifications.Integrations.Configuration; using Altinn.Notifications.Integrations.Profile; +using Altinn.Notifications.Integrations.Register; using Microsoft.Extensions.Options; @@ -69,8 +70,8 @@ public async Task> GetUserRegisteredOrganization } string responseContent = await response.Content.ReadAsStringAsync(); - List? contactPoints = JsonSerializer.Deserialize>(responseContent, JsonSerializerOptionsProvider.Options)!; + OrgContactPointsList? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!; - return contactPoints!; + return contactPoints.ContactPointsList!; } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs index a83bb11f..bbeb77df 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs @@ -12,6 +12,7 @@ using Altinn.Notifications.Integrations.Clients; using Altinn.Notifications.Integrations.Configuration; using Altinn.Notifications.Integrations.Profile; +using Altinn.Notifications.Integrations.Register; using Microsoft.Extensions.Options; @@ -162,13 +163,16 @@ private Task GetUnitProfileResponse(UnitContactPointLookup switch (lookup.ResourceId) { case "no-matches": - contentData = new List(); + contentData = new OrgContactPointsList(); break; case "some-matches": - contentData = new List + contentData = new OrgContactPointsList() { - new() { OrganizationNumber = "123456789", PartyId = 56789, UserContactPoints = [new() { UserId = 20001 }] }, - new() { OrganizationNumber = "987654321", PartyId = 54321, UserContactPoints = [new() { UserId = 20001 }] } + ContactPointsList = new List + { + new() { OrganizationNumber = "123456789", PartyId = 56789, UserContactPoints = [new() { UserId = 20001 }] }, + new() { OrganizationNumber = "987654321", PartyId = 54321, UserContactPoints = [new() { UserId = 20001 }] } + } }; break; case "error-resource": From 06ffff908fb5f97acafb4fdcd1c5f3d28705dd3d Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Fri, 24 May 2024 08:25:06 +0200 Subject: [PATCH 30/36] GetUserRegisteredOrganizationContactPoints -> GetUserRegisteredContactPoints --- .../Integrations/IProfileClient.cs | 2 +- .../Services/ContactPointService.cs | 2 +- .../Profile/ProfileClient.cs | 4 ++-- .../TestingServices/ContactPointServiceTests.cs | 6 +++--- .../Profile/ProfileClientTests.cs | 14 +++++++------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs index cab0f2df..37ba1c8c 100644 --- a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs @@ -20,5 +20,5 @@ public interface IProfileClient /// The set or organizations to retrieve contact points for /// The id of the resource to look up contact points for /// A list of organiation contact points containing user registered contact points - public Task> GetUserRegisteredOrganizationContactPoints(List organizationNumbers, string resourceId); + public Task> GetUserRegisteredContactPoints(List organizationNumbers, string resourceId); } diff --git a/src/Altinn.Notifications.Core/Services/ContactPointService.cs b/src/Altinn.Notifications.Core/Services/ContactPointService.cs index fbc2f5f8..8170235e 100644 --- a/src/Altinn.Notifications.Core/Services/ContactPointService.cs +++ b/src/Altinn.Notifications.Core/Services/ContactPointService.cs @@ -166,7 +166,7 @@ private async Task> LookupOrganizationContactPoi if (!string.IsNullOrEmpty(resourceId)) { - var allUserContactPoints = await _profileClient.GetUserRegisteredOrganizationContactPoints(orgNos, resourceId); + var allUserContactPoints = await _profileClient.GetUserRegisteredContactPoints(orgNos, resourceId); authorizedUserContactPoints = await _authorizationService.AuthorizeUserContactPointsForResource(allUserContactPoints, resourceId); } diff --git a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs index ccea1fe3..023bd30e 100644 --- a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs +++ b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs @@ -52,7 +52,7 @@ public async Task> GetUserContactPoints(List nat } /// - public async Task> GetUserRegisteredOrganizationContactPoints(List organizationNumbers, string resourceId) + public async Task> GetUserRegisteredContactPoints(List organizationNumbers, string resourceId) { var lookupObject = new UnitContactPointLookup() { @@ -66,7 +66,7 @@ public async Task> GetUserRegisteredOrganization if (!response.IsSuccessStatusCode) { - throw new PlatformHttpException(response, $"ProfileClient.GetUserRegisteredOrganizationContactPoints failed with status code {response.StatusCode}"); + throw new PlatformHttpException(response, $"ProfileClient.GetUserRegisteredContactPoints failed with status code {response.StatusCode}"); } string responseContent = await response.Content.ReadAsStringAsync(); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs index 042c8a64..4945270d 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/ContactPointServiceTests.cs @@ -113,7 +113,7 @@ public async Task AddSmsContactPoints_OrganizationNumberAndResourceAvailable_Aut var profileClientMock = new Mock(); profileClientMock - .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .Setup(p => p.GetUserRegisteredContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) .ReturnsAsync([ new OrganizationContactPoints() { @@ -248,7 +248,7 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_N var profileClientMock = new Mock(); profileClientMock - .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .Setup(p => p.GetUserRegisteredContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) .ReturnsAsync([ new OrganizationContactPoints() { @@ -320,7 +320,7 @@ public async Task AddEmailContactPoints_OrganizationNumberAndResourceAvailable_A var profileClientMock = new Mock(); profileClientMock - .Setup(p => p.GetUserRegisteredOrganizationContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) + .Setup(p => p.GetUserRegisteredContactPoints(It.IsAny>(), It.Is(s => s.Equals("urn:altinn:resource")))) .ReturnsAsync([ new OrganizationContactPoints() { diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs index bbeb77df..7096ade2 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs @@ -89,20 +89,20 @@ public async Task GetUserContactPoints_FailureResponse_ExceptionIsThrown() } [Fact] - public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_NoMatches() + public async Task GetUserRegisteredContactPoints_SuccessResponse_NoMatches() { // Act - List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints(["12345678", "98754321"], "no-matches"); + List actual = await _profileClient.GetUserRegisteredContactPoints(["12345678", "98754321"], "no-matches"); // Assert Assert.Empty(actual); } [Fact] - public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_TwoListElementsReturned() + public async Task GetUserRegisteredContactPoints_SuccessResponse_TwoListElementsReturned() { // Act - List actual = await _profileClient.GetUserRegisteredOrganizationContactPoints(["12345678", "98754321"], "some-matches"); + List actual = await _profileClient.GetUserRegisteredContactPoints(["12345678", "98754321"], "some-matches"); // Assert Assert.Equal(2, actual.Count); @@ -110,13 +110,13 @@ public async Task GetUserRegisteredOrganizationContactPoints_SuccessResponse_Two } [Fact] - public async Task GetUserRegisteredOrganizationContactPoints_FailureResponse_ExceptionIsThrown() + public async Task GetUserRegisteredContactPoints_FailureResponse_ExceptionIsThrown() { // Act var exception = await Assert.ThrowsAsync( - async () => await _profileClient.GetUserRegisteredOrganizationContactPoints(["12345678", "98754321"], "error-resource")); + async () => await _profileClient.GetUserRegisteredContactPoints(["12345678", "98754321"], "error-resource")); - Assert.StartsWith("ProfileClient.GetUserRegisteredOrganizationContactPoints failed with status code", exception.Message); + Assert.StartsWith("ProfileClient.GetUserRegisteredContactPoints failed with status code", exception.Message); Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); } From 7ff58d8c60c6905dab46e67bd5213ed680978fb1 Mon Sep 17 00:00:00 2001 From: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com> Date: Mon, 27 May 2024 09:37:41 +0200 Subject: [PATCH 31/36] Update src/Altinn.Notifications.Core/Integrations/IProfileClient.cs Co-authored-by: Terje Holene --- src/Altinn.Notifications.Core/Integrations/IProfileClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs index 37ba1c8c..0f786e9d 100644 --- a/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs +++ b/src/Altinn.Notifications.Core/Integrations/IProfileClient.cs @@ -15,7 +15,7 @@ public interface IProfileClient public Task> GetUserContactPoints(List nationalIdentityNumbers); /// - /// Retrieves the user registered contact points for a list of organization corresponding to a list of organization numbers + /// Retrieves the user registered contact points for a list of organizations identified by organization numbers /// /// The set or organizations to retrieve contact points for /// The id of the resource to look up contact points for From 9aa334cc8d240cc3969e6d470611c4018aa4a0e0 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Mon, 27 May 2024 09:50:18 +0200 Subject: [PATCH 32/36] fixed some pr comments --- .../Profile/ProfileClient.cs | 8 ++++---- .../Profile/ProfileClientTests.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs index 023bd30e..b5bcdf4b 100644 --- a/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs +++ b/src/Altinn.Notifications.Integrations/Profile/ProfileClient.cs @@ -47,7 +47,7 @@ public async Task> GetUserContactPoints(List nat } string responseContent = await response.Content.ReadAsStringAsync(); - List? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; + List contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!.ContactPointsList; return contactPoints!; } @@ -70,8 +70,8 @@ public async Task> GetUserRegisteredContactPoint } string responseContent = await response.Content.ReadAsStringAsync(); - OrgContactPointsList? contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!; - - return contactPoints.ContactPointsList!; + OrgContactPointsList contactPoints = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsProvider.Options)!; + + return contactPoints.ContactPointsList; } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs index 7096ade2..3141c832 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Integrations/Profile/ProfileClientTests.cs @@ -33,12 +33,12 @@ public ProfileClientTests() { var profileHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => { - if (request!.RequestUri!.AbsolutePath.EndsWith("users/contactpoint/lookup")) + if (request.RequestUri!.AbsolutePath.EndsWith("users/contactpoint/lookup")) { UserContactPointLookup? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), JsonSerializerOptionsProvider.Options); return await GetUserProfileResponse(lookup!); } - else if (request!.RequestUri!.AbsolutePath.EndsWith("units/contactpoint/lookup")) + else if (request.RequestUri!.AbsolutePath.EndsWith("units/contactpoint/lookup")) { UnitContactPointLookup? lookup = JsonSerializer.Deserialize(await request!.Content!.ReadAsStringAsync(token), JsonSerializerOptionsProvider.Options); return await GetUnitProfileResponse(lookup!); @@ -117,7 +117,7 @@ public async Task GetUserRegisteredContactPoints_FailureResponse_ExceptionIsThro async () => await _profileClient.GetUserRegisteredContactPoints(["12345678", "98754321"], "error-resource")); Assert.StartsWith("ProfileClient.GetUserRegisteredContactPoints failed with status code", exception.Message); - Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response?.StatusCode); + Assert.Equal(HttpStatusCode.ServiceUnavailable, exception.Response.StatusCode); } private Task GetUserProfileResponse(UserContactPointLookup lookup) From c9b48c12a0ed8f864e6513765a1b899ee590b3b1 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Mon, 27 May 2024 12:28:09 +0200 Subject: [PATCH 33/36] set parameters as required --- src/Altinn.Notifications/Mappers/OrderMapper.cs | 8 ++++++-- .../Models/EmailNotificationOrderRequestExt.cs | 7 +++++-- .../Models/NotificationOrderRequestBaseExt.cs | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Altinn.Notifications/Mappers/OrderMapper.cs b/src/Altinn.Notifications/Mappers/OrderMapper.cs index 3a9287c8..7e1c57a8 100644 --- a/src/Altinn.Notifications/Mappers/OrderMapper.cs +++ b/src/Altinn.Notifications/Mappers/OrderMapper.cs @@ -18,7 +18,11 @@ public static class OrderMapper /// public static NotificationOrderRequest MapToOrderRequest(this EmailNotificationOrderRequestExt extRequest, string creator) { - var emailTemplate = new EmailTemplate(null, extRequest.Subject, extRequest.Body, (EmailContentType)extRequest.ContentType); + var emailTemplate = new EmailTemplate( + null, + extRequest.Subject, + extRequest.Body, + (EmailContentType?)extRequest.ContentType ?? EmailContentType.Plain); List recipients = extRequest.Recipients @@ -217,7 +221,7 @@ private static BaseNotificationOrderExt MapBaseNotificationOrder(this BaseNotifi orderExt.RequestedSendTime = order.RequestedSendTime; orderExt.IgnoreReservation = order.IgnoreReservation; orderExt.ResourceId = order.ResourceId; - + return orderExt; } diff --git a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs index a6569b96..61a87e53 100644 --- a/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/EmailNotificationOrderRequestExt.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; using System.Text.Json.Serialization; namespace Altinn.Notifications.Models; @@ -15,19 +16,21 @@ public class EmailNotificationOrderRequestExt : NotificationOrderRequestBaseExt /// Gets or sets the subject of the email /// [JsonPropertyName("subject")] + [Required] public string Subject { get; set; } = string.Empty; /// /// Gets or sets the body of the email /// [JsonPropertyName("body")] + [Required] public string Body { get; set; } = string.Empty; /// /// Gets or sets the content type of the email /// [JsonPropertyName("contentType")] - public EmailContentTypeExt ContentType { get; set; } = EmailContentTypeExt.Plain; + public EmailContentTypeExt? ContentType { get; set; } /// /// Json serialized the diff --git a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs index fbc57ff1..94ae1a21 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderRequestBaseExt.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; namespace Altinn.Notifications.Models; @@ -24,6 +24,7 @@ public class NotificationOrderRequestBaseExt /// Gets or sets the list of recipients /// [JsonPropertyName("recipients")] + [Required] public List Recipients { get; set; } = new List(); /// From f6cfac3e33a9da89c5c52638e0571fc8f6ce4d56 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Mon, 27 May 2024 12:30:07 +0200 Subject: [PATCH 34/36] added required to sms model --- .../Models/SmsNotificationOrderRequestExt.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs index 248d05ba..dafc085b 100644 --- a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; using System.Text.Json.Serialization; namespace Altinn.Notifications.Models; @@ -21,6 +22,7 @@ public class SmsNotificationOrderRequestExt : NotificationOrderRequestBaseExt /// Gets or sets the body of the SMS /// [JsonPropertyName("body")] + [Required] public string Body { get; set; } = string.Empty; /// From e704834ea92580d41724f7baca2c4385182e18fc Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Mon, 27 May 2024 15:21:21 +0200 Subject: [PATCH 35/36] fixed code smell async validation --- .../Controllers/EmailNotificationOrdersController.cs | 2 +- .../Controllers/SmsNotificationOrdersController.cs | 2 +- .../Models/SmsNotificationOrderRequestExt.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs index 4948de71..336c7f61 100644 --- a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs +++ b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs @@ -54,7 +54,7 @@ public EmailNotificationOrdersController(IValidator> Post(EmailNotificationOrderRequestExt emailNotificationOrderRequest) { - var validationResult = _validator.Validate(emailNotificationOrderRequest); + var validationResult = await _validator.ValidateAsync(emailNotificationOrderRequest); if (!validationResult.IsValid) { validationResult.AddToModelState(this.ModelState); diff --git a/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs b/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs index acc36760..7d6fda7c 100644 --- a/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs +++ b/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs @@ -54,7 +54,7 @@ public SmsNotificationOrdersController(IValidator> Post(SmsNotificationOrderRequestExt smsNotificationOrderRequest) { - FluentValidation.Results.ValidationResult validationResult = _validator.Validate(smsNotificationOrderRequest); + FluentValidation.Results.ValidationResult validationResult = await _validator.ValidateAsync(smsNotificationOrderRequest); if (!validationResult.IsValid) { validationResult.AddToModelState(this.ModelState); diff --git a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs index dafc085b..7747571c 100644 --- a/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs +++ b/src/Altinn.Notifications/Models/SmsNotificationOrderRequestExt.cs @@ -16,7 +16,7 @@ public class SmsNotificationOrderRequestExt : NotificationOrderRequestBaseExt /// Gets or sets the sender number of the SMS /// [JsonPropertyName("senderNumber")] - public string SenderNumber { get; set; } = string.Empty; + public string? SenderNumber { get; set; } /// /// Gets or sets the body of the SMS From 9832061983a61700caf32f3c5979798c78abb1f5 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Tue, 28 May 2024 10:44:56 +0200 Subject: [PATCH 36/36] rolled back async validation. Caused null reference exception --- .../Controllers/EmailNotificationOrdersController.cs | 2 +- .../Controllers/SmsNotificationOrdersController.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs index 336c7f61..4948de71 100644 --- a/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs +++ b/src/Altinn.Notifications/Controllers/EmailNotificationOrdersController.cs @@ -54,7 +54,7 @@ public EmailNotificationOrdersController(IValidator> Post(EmailNotificationOrderRequestExt emailNotificationOrderRequest) { - var validationResult = await _validator.ValidateAsync(emailNotificationOrderRequest); + var validationResult = _validator.Validate(emailNotificationOrderRequest); if (!validationResult.IsValid) { validationResult.AddToModelState(this.ModelState); diff --git a/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs b/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs index 7d6fda7c..27d906f8 100644 --- a/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs +++ b/src/Altinn.Notifications/Controllers/SmsNotificationOrdersController.cs @@ -6,6 +6,7 @@ using Altinn.Notifications.Validators; using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -54,7 +55,7 @@ public SmsNotificationOrdersController(IValidator> Post(SmsNotificationOrderRequestExt smsNotificationOrderRequest) { - FluentValidation.Results.ValidationResult validationResult = await _validator.ValidateAsync(smsNotificationOrderRequest); + ValidationResult validationResult = _validator.Validate(smsNotificationOrderRequest); if (!validationResult.IsValid) { validationResult.AddToModelState(this.ModelState);