From c357a93e95b1c7f758a89959ff66596170b437cc Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Tue, 30 Jan 2024 18:02:50 +0100 Subject: [PATCH 01/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Proxy always enabled, also for protocol WcfSoapWithOpenIdConnect. Added Trisoft.ISHRemote.OpenApiAM10 based on released ISHAM 15.0.0... Later update NuGet on both projects --- Doc/TheExecution-ISHRemote-8.0.md | 5 - Source/ISHRemote/ISHRemote.sln | 9 + .../OpenApiAM10.json | 3662 +++++++++++++++++ .../Trisoft.ISHRemote.OpenApiAM10.csproj | 32 + .../Cmdlets/Session/NewIshSession.Tests.ps1 | 3 +- ...ShareWcfSoapWithOpenIdConnectConnection.cs | 1 - .../Objects/Public/IshSession.cs | 8 +- .../Trisoft.ISHRemote.csproj | 1 + 8 files changed, 3713 insertions(+), 8 deletions(-) create mode 100644 Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json create mode 100644 Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj diff --git a/Doc/TheExecution-ISHRemote-8.0.md b/Doc/TheExecution-ISHRemote-8.0.md index c7956cfb..72b3f68b 100644 --- a/Doc/TheExecution-ISHRemote-8.0.md +++ b/Doc/TheExecution-ISHRemote-8.0.md @@ -207,14 +207,9 @@ For whoever stumbles on this transitive package dependency of `System.Runtime.Co * Authentication over Client Credentials Flow with expired `-ClientId`/`-ClientSecret` combination will . Please recycle expired client/secret on your Access Management User Profile (ISHAM). * Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, but not mapped in the CMS to a User Profile over `FISHEXTERNALID` will . Please make sure that the client (which you can find on the Access Management User Profile) is added in Organize Space on one CMS User Profile in the comma-seperated External Id field. * Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, and mapped in the CMS to a User Profile over `FISHEXTERNALID` which is disabled will . Please make sure in Organize Space that the one CMS User Profile holding the client in the External Id field is an enabled profile. - - * Help * $ishSessionA = New-IshSession -WsBaseUrl "https://example.com/ISHWSPROD/" -PSCredential "Admin" --> `-PSCredential Admin` only works for `-Protocol WcfSoapWithWsTrust` so it is an outdated sample ... all New-IshSession should be reviewed. -* Known Issues - * Refresh Token is not used to refresh the Bearer Token in the background, it is used to refresh when the next cmdlet is triggered before expiration. (already in release notes, can be removed or needs updating) - # Next - Should Have Section * Test refresh with short expiration diff --git a/Source/ISHRemote/ISHRemote.sln b/Source/ISHRemote/ISHRemote.sln index e10e340f..3aaea3ba 100644 --- a/Source/ISHRemote/ISHRemote.sln +++ b/Source/ISHRemote/ISHRemote.sln @@ -4,9 +4,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.0.32802.463 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote", "Trisoft.ISHRemote\Trisoft.ISHRemote.csproj", "{A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}" + ProjectSection(ProjectDependencies) = postProject + {14F8CA44-6ACF-442B-B436-33D829B4CB83} = {14F8CA44-6ACF-442B-B436-33D829B4CB83} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.csproj", "{E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.csproj", "{14F8CA44-6ACF-442B-B436-33D829B4CB83}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +26,10 @@ Global {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Release|Any CPU.Build.0 = Release|Any CPU + {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json new file mode 100644 index 00000000..0e492916 --- /dev/null +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json @@ -0,0 +1,3662 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Access Management API", + "description": "Access Management provides implementers with a simplified approach to identity management and gives administrators a central location for ongoing management of access to applications. ", + "version": "v1" + }, + "servers": [ + { + "url": "/ISHAM" + } + ], + "paths": { + "/api/v1/ApiResources": { + "get": { + "tags": [ + "ApiResources" + ], + "summary": "Gets a list of all API resources in the system.", + "operationId": "ApiResources_Get", + "responses": { + "200": { + "description": "Returns a list of API resources.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResource" + } + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/ApiResources/{id}": { + "get": { + "tags": [ + "ApiResources" + ], + "summary": "Gets the API resource with the given ID.", + "operationId": "ApiResources_GetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the API resource to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested API resource.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResource" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The API resource does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Applications": { + "get": { + "tags": [ + "Applications" + ], + "summary": "Gets a list of all applications in the system.", + "operationId": "Applications_Get", + "responses": { + "200": { + "description": "Returns a list of applications.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Application" + } + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Applications" + ], + "summary": "Creates a new application.", + "operationId": "Applications_Create", + "requestBody": { + "description": "The application to create.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + } + }, + "responses": { + "201": { + "description": "The application was created successfully. The Location header contains the URL to the new item.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Applications/{id}": { + "get": { + "tags": [ + "Applications" + ], + "summary": "Gets the application with the given ID.", + "operationId": "Applications_GetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the application to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested application.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The application does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Applications" + ], + "summary": "Updates an existing application.", + "operationId": "Applications_Update", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the application to update.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The new data of the application.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated application.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The application does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Applications" + ], + "summary": "Permanently deletes an existing application.", + "operationId": "Applications_Delete", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the application to delete.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "The application was successfully deleted." + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The application does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/AuditLogs": { + "get": { + "tags": [ + "AuditLogs" + ], + "summary": "Gets a list of audit log entries matching the given filter.", + "operationId": "AuditLogs_GetByFilter", + "parameters": [ + { + "name": "UserName", + "in": "query", + "schema": { + "maxLength": 256, + "type": "string" + } + }, + { + "name": "FromDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2024-01-17 00:00:00.000" + }, + { + "name": "ToDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "2024-01-17 23:59:59.999" + }, + { + "name": "EntityType", + "in": "query", + "schema": { + "maxItems": 10, + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "EntityName", + "in": "query", + "schema": { + "maxLength": 256, + "type": "string" + } + }, + { + "name": "Action", + "in": "query", + "schema": { + "maxItems": 5, + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityActionType" + } + } + }, + { + "name": "Limit", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns a list of audit log entries.", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLog" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLog" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLog" + } + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/AuditLogs/{id}": { + "get": { + "tags": [ + "AuditLogs" + ], + "summary": "Gets the audit log entry with the given ID.", + "operationId": "AuditLogs_GetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the audit log entry to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested audit log entry.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/AuditLogEntry" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuditLogEntry" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/AuditLogEntry" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The audit log entry does not exist.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/IdentityProviders/availableIdpTypes": { + "get": { + "tags": [ + "IdentityProviders" + ], + "summary": "Gets a list of available identity provider types.", + "operationId": "IdentityProviders_GetAvailableIdpTypes", + "responses": { + "200": { + "description": "Returns the list of available identity provider types.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProviderTypeOption" + } + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/IdentityProviders": { + "get": { + "tags": [ + "IdentityProviders" + ], + "summary": "Gets a list of all identity providers in the system.", + "operationId": "IdentityProviders_Get", + "responses": { + "200": { + "description": "Returns a list of identity providers.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProvider" + } + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "IdentityProviders" + ], + "summary": "Creates a new identity provider.", + "operationId": "IdentityProviders_Create", + "requestBody": { + "description": "The identity provider to create.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + } + } + }, + "responses": { + "201": { + "description": "The identity provider was created successfully. The Location header contains the URL to the new item.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/IdentityProviders/{id}": { + "get": { + "tags": [ + "IdentityProviders" + ], + "summary": "Gets the identity provider with the given ID.", + "operationId": "IdentityProviders_GetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the identity provider to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested identity provider.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The identity provider does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "IdentityProviders" + ], + "summary": "Updates an existing identity provider.", + "operationId": "IdentityProviders_Update", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the identity provider to update.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The new data of the identity provider.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated identity provider.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProvider" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The identity provider does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "IdentityProviders" + ], + "summary": "Permanently deletes an existing identity provider.", + "operationId": "IdentityProviders_Delete", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the identity provider to delete.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "The identity provider was successfully deleted." + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The identity provider does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/IdentityProviders/icon/{name}": { + "get": { + "tags": [ + "IdentityProviders" + ], + "summary": "Gets the icon for a given identity provider.", + "description": "If no icon has been specified for the identity provider, a default icon is returned.", + "operationId": "IdentityProviders_GetIcon", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "The name of the identity provider to get an icon for.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns the icon as an image." + }, + "302": { + "description": "Redirects to a local image." + }, + "500": { + "description": "An unexpected error occurred." + } + } + } + }, + "/api/v1/IdentityProviders/loginOptions": { + "get": { + "tags": [ + "IdentityProviders" + ], + "summary": "Gets a list of all login possibilities currently available.", + "operationId": "IdentityProviders_GetLoginOptions", + "responses": { + "200": { + "description": "Returns the list of login options.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LoginOption" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/IdentityProviders/getParametersForIdentityProviderType/{providerType}": { + "get": { + "tags": [ + "IdentityProviders" + ], + "summary": "Gets a list of parameters specific to a given type of identity provider.", + "operationId": "IdentityProviders_GetParametersForIdentityProviderType", + "parameters": [ + { + "name": "providerType", + "in": "path", + "description": "The type of identity provider to retrieve the parameters for.", + "required": true, + "schema": { + "$ref": "#/components/schemas/IdentityProviderType" + } + } + ], + "responses": { + "200": { + "description": "Returns the list of parameters for the given type.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderParameters" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/ServiceAccounts": { + "get": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Gets a list of all service accounts in the system.", + "operationId": "ServiceAccounts_Get", + "responses": { + "200": { + "description": "Returns a list of service accounts.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceAccount" + } + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Creates a new service account.", + "operationId": "ServiceAccounts_Create", + "requestBody": { + "description": "The data of the service account to create.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + } + } + }, + "responses": { + "201": { + "description": "The service account was created successfully. The Location header contains the URL to the new item.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/ServiceAccounts/{id}": { + "get": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Gets the service account with the given ID.", + "operationId": "ServiceAccounts_GetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the service account to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested service account.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The service account does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Updates an existing service account.", + "operationId": "ServiceAccounts_Update", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the service account to update.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The new data of the service account.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated service account.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceAccount" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The service account does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Permanently deletes an existing service account.", + "operationId": "ServiceAccounts_Delete", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the service account to delete.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "The service account was successfully deleted." + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The service account does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/ServiceAccounts/{serviceAccountId}/generateClientSecret": { + "post": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Generates a new client secret that can be used to log in as a given service account.", + "operationId": "ServiceAccounts_GenerateClientSecret", + "parameters": [ + { + "name": "serviceAccountId", + "in": "path", + "description": "The ID of the service account.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the new client secret.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The service account does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/ServiceAccounts/{serviceAccountId}/clientSecrets/{secretId}": { + "put": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Updates an existing client secret.", + "operationId": "ServiceAccounts_UpdateClientSecret", + "parameters": [ + { + "name": "serviceAccountId", + "in": "path", + "description": "The ID of the service account.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "secretId", + "in": "path", + "description": "The ID of the secret to update.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The new data of the client secret.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated client secret.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The service account or client secret does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "ServiceAccounts" + ], + "summary": "Permanently deletes an existing client secret from a service account.", + "operationId": "ServiceAccounts_DeleteClientSecret", + "parameters": [ + { + "name": "serviceAccountId", + "in": "path", + "description": "The ID of the service account.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "secretId", + "in": "path", + "description": "The ID of the secret to delete.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "The client secret was successfully deleted." + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The service account or client secret does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/System/getLoginStatus": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets the login status of the current user.", + "operationId": "System_GetLoginStatus", + "responses": { + "200": { + "description": "Returns either None, LoggedIn, or Anonymous (if there are no IDPs yet or the configuration forces an anonymous state).", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LoginStatus" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginStatus" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LoginStatus" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/System/userInfo": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets information about the current user.", + "operationId": "System_GetUserInfo", + "responses": { + "200": { + "description": "Returns information about the current user.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UserInfo" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserInfo" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserInfo" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/System/supportedCultures": { + "get": { + "tags": [ + "System" + ], + "summary": "Gets a list of all supported languages and a list of all supported regions.", + "operationId": "System_GetSupportedCultures", + "responses": { + "200": { + "description": "Returns an object with a list of all supported languages and a separate list of supported regions.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/SupportedCultures" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportedCultures" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SupportedCultures" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Users": { + "get": { + "tags": [ + "Users" + ], + "summary": "Gets a list of all users in the system.", + "operationId": "Users_Get", + "responses": { + "200": { + "description": "Returns a list of users.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Users/{id}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Gets the user with the given ID.", + "operationId": "Users_GetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the user to retrieve.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The user does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Users" + ], + "summary": "Updates an existing user.", + "operationId": "Users_Update", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the user to update.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The new data of the user.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The user does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Users" + ], + "summary": "Permanently deletes an existing user.", + "description": "Note that deleted users will automatically be recreated if they log into Access Management afterwards.\r\nTo completely remove users, first remove them from the external identity provider.", + "operationId": "Users_Delete", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the user to delete.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "The user was successfully deleted." + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The user does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Users/{id}/deactivate": { + "put": { + "tags": [ + "Users" + ], + "summary": "Deactivates a user, so they can no longer log in.", + "operationId": "Users_DeactivateUser", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the user to deactivate.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "The user was successfully deactivated." + }, + "404": { + "description": "The user does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Users/{id}/activate": { + "put": { + "tags": [ + "Users" + ], + "summary": "Re-activates a deactivated user so they can log in again.", + "operationId": "Users_ActivateUser", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the user to activate.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "The user was successfully activated." + }, + "404": { + "description": "The user does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/Users/{userId}/generateClientSecret": { + "post": { + "tags": [ + "Users" + ], + "summary": "Generates a new client secret that can be used to log in as a given user.\r\n(Client secrets for users are deprecated. Please use service accounts instead.)", + "operationId": "Users_GenerateClientSecret", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the new client secret.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The user does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "deprecated": true + } + }, + "/api/v1/Users/{userId}/clientSecrets/{secretId}": { + "put": { + "tags": [ + "Users" + ], + "summary": "Updates an existing client secret.\r\n(Client secrets for users are deprecated. Please use service accounts instead.)", + "operationId": "Users_UpdateClientSecret", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "secretId", + "in": "path", + "description": "The ID of the secret to update.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The new data of the client secret.", + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated client secret.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserClientSecret" + } + } + } + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The user or client secret does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "deprecated": true + }, + "delete": { + "tags": [ + "Users" + ], + "summary": "Permanently deletes an existing client secret from a user.\r\n(Client secrets for users are deprecated. Please use service accounts instead.)", + "operationId": "Users_DeleteClientSecret", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The ID of the user.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "secretId", + "in": "path", + "description": "The ID of the secret to delete.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "The client secret was successfully deleted." + }, + "400": { + "description": "A validation error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The user or client secret does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "An unexpected error occurred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "deprecated": true + } + } + }, + "components": { + "schemas": { + "ApiResource": { + "required": [ + "key", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "key": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[a-zA-Z0-9-_.]*$", + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceRole" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "ApiResourceLink": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "ApiResourceRole": { + "required": [ + "key", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "apiResource": { + "$ref": "#/components/schemas/ApiResource" + } + }, + "additionalProperties": false + }, + "ApiResourceRoleLink": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "Application": { + "required": [ + "allowedAuthenticationFlowType", + "clientId", + "name", + "redirectUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.Readonly on Update", + "format": "int32", + "readOnly": true + }, + "createdBy": { + "$ref": "#/components/schemas/UserLink" + }, + "modifiedBy": { + "$ref": "#/components/schemas/UserLink" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "clientId": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "redirectUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowedAuthenticationFlowType": { + "$ref": "#/components/schemas/AuthenticationFlowType" + } + }, + "additionalProperties": false + }, + "ApplicationLink": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "AuditLog": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.", + "format": "int32", + "readOnly": true + }, + "user": { + "$ref": "#/components/schemas/Link" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "entityType": { + "type": "string", + "nullable": true + }, + "entity": { + "$ref": "#/components/schemas/Link" + }, + "action": { + "$ref": "#/components/schemas/EntityActionType" + } + }, + "additionalProperties": false + }, + "AuditLogEntry": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.", + "format": "int32", + "readOnly": true + }, + "user": { + "$ref": "#/components/schemas/Link" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "entityType": { + "type": "string", + "nullable": true + }, + "entityId": { + "type": "integer", + "format": "int32" + }, + "entityData": { + "nullable": true + }, + "action": { + "$ref": "#/components/schemas/EntityActionType" + } + }, + "additionalProperties": false + }, + "AuthenticationFlowType": { + "enum": [ + "AuthorizationCodeWithPkce", + "AuthorizationCode", + "Implicit" + ], + "type": "string" + }, + "ClaimBasedAccessControlEntry": { + "type": "object", + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationLink" + }, + "nullable": true + }, + "apiResources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceLink" + }, + "nullable": true + }, + "apiResourceRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceRoleLink" + }, + "nullable": true + }, + "claimType": { + "maxLength": 256, + "type": "string", + "description": "Gets or sets Claim type coming from the IdP.", + "nullable": true + }, + "claimValue": { + "maxLength": 256, + "type": "string", + "description": "Gets or sets Claim value coming from the IdP.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Represents idp claim access control for applications , resources and roles." + }, + "EntityActionType": { + "enum": [ + "Deleted", + "Updated", + "Added", + "Activated", + "Deactivated" + ], + "type": "string" + }, + "ErrorMessage": { + "type": "object", + "properties": { + "code": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "localizedMessage": { + "type": "string", + "nullable": true + }, + "target": { + "type": "string", + "nullable": true + }, + "details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ErrorMessage" + }, + "nullable": true + }, + "innerError": { + "$ref": "#/components/schemas/IInnerError" + }, + "stackTrace": { + "type": "string", + "nullable": true + }, + "innerException": { + "$ref": "#/components/schemas/ErrorMessage" + } + }, + "additionalProperties": false + }, + "ErrorResponse": { + "type": "object", + "properties": { + "errorMessage": { + "$ref": "#/components/schemas/ErrorMessage" + } + }, + "additionalProperties": false + }, + "IInnerError": { + "type": "object", + "properties": { + "code": { + "type": "string", + "nullable": true + }, + "innerError": { + "$ref": "#/components/schemas/IInnerError" + } + }, + "additionalProperties": false + }, + "IdentityProvider": { + "required": [ + "key", + "name", + "parameters", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.", + "format": "int32", + "readOnly": true + }, + "createdBy": { + "$ref": "#/components/schemas/UserLink" + }, + "modifiedBy": { + "$ref": "#/components/schemas/UserLink" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "key": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-zA-Z0-9_]*$", + "type": "string", + "description": "Provider name.Readonly on Update" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider display name." + }, + "isEnabled": { + "type": "boolean", + "description": "Specifies if provider is enabled.", + "default": true + }, + "description": { + "maxLength": 2048, + "type": "string", + "description": "Provider description.", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/IdentityProviderType" + }, + "iconUrl": { + "type": "string", + "description": "The external URL of the icon to use for this provider.", + "nullable": true + }, + "iconViewUrl": { + "type": "string", + "description": "The URL to view the icon for this provider.", + "nullable": true, + "readOnly": true + }, + "accessControlList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClaimBasedAccessControlEntry" + }, + "description": "Access Control List", + "nullable": true + }, + "forwardedClaims": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Forwarded Claims", + "nullable": true + }, + "parameters": { + "$ref": "#/components/schemas/IdentityProviderParameters" + }, + "redirectUrl": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "postLogoutRedirectUrl": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "validateUrl": { + "type": "string", + "description": "Gets or sets the URL to use to validate that this Tridion.AccessManagement.Model.V1.IdentityProvider is configured correctly.", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Represents the identity provider." + }, + "IdentityProviderLink": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Readonly on Update" + }, + "IdentityProviderParameters": { + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/LdapParameters" + }, + { + "$ref": "#/components/schemas/OpenIdParameters" + }, + { + "$ref": "#/components/schemas/SamlParameters" + }, + { + "$ref": "#/components/schemas/WindowsParameters" + } + ], + "properties": { + "$type": { + "type": "string", + "readOnly": true + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type" + } + }, + "IdentityProviderType": { + "enum": [ + "OpenIdConnect", + "SAML2P", + "LDAP", + "Windows" + ], + "type": "string", + "description": "Readonly on Update", + "readOnly": true + }, + "IdentityProviderTypeOption": { + "type": "object", + "properties": { + "typeId": { + "$ref": "#/components/schemas/IdentityProviderType" + }, + "name": { + "type": "string", + "description": "Localized human readable name.", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false, + "description": "Represents the Identity provider type option description." + }, + "LdapParameters": { + "required": [ + "fullNameClaim", + "port", + "searchAccount", + "searchAccountPassword", + "serverAddress", + "userBaseDn", + "usernameClaim", + "useSsl" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/IdentityProviderParameters" + } + ], + "properties": { + "separator": { + "maxLength": 1, + "type": "string", + "description": "Separator to join IdP name and username.", + "nullable": true + }, + "usernameClaim": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider claim type to use for username." + }, + "fullNameClaim": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider claim type to use for Full name." + }, + "serverAddress": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "port": { + "type": "integer", + "format": "int32" + }, + "useSsl": { + "type": "boolean" + }, + "searchAccount": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "searchAccountPassword": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "format": "password" + }, + "userBaseDn": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "groupBaseDn": { + "maxLength": 256, + "type": "string", + "nullable": true + }, + "groupMemberAttribute": { + "maxLength": 256, + "type": "string", + "nullable": true + }, + "additionalAttributes": { + "maxLength": 256, + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LoginOption": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "key": { + "type": "string", + "nullable": true + }, + "loginTriggerUrl": { + "type": "string", + "nullable": true + }, + "iconUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LoginStatus": { + "enum": [ + "None", + "Anonymous", + "LoggedIn" + ], + "type": "string" + }, + "OpenIdParameters": { + "required": [ + "authority", + "clientId", + "fullNameClaim", + "usernameClaim" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/IdentityProviderParameters" + } + ], + "properties": { + "separator": { + "maxLength": 1, + "type": "string", + "description": "Separator to join IdP name and username.", + "nullable": true + }, + "usernameClaim": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider claim type to use for username." + }, + "fullNameClaim": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider claim type to use for Full name." + }, + "authority": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "format": "uri" + }, + "clientId": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "clientSecret": { + "maxLength": 256, + "type": "string", + "format": "password", + "nullable": true + }, + "endSessionEndpoint": { + "maxLength": 256, + "type": "string", + "format": "uri", + "nullable": true + }, + "responseType": { + "maxLength": 256, + "type": "string", + "default": "id_token", + "nullable": true + }, + "sendIdTokenHintDuringLogout": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "SamlParameters": { + "required": [ + "certificates", + "fullNameClaim", + "issuerName", + "serviceProviderName", + "singleLogoutServiceUrl", + "singleSignOnServiceUrl", + "usernameClaim" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/IdentityProviderParameters" + } + ], + "properties": { + "separator": { + "maxLength": 1, + "type": "string", + "description": "Separator to join IdP name and username.", + "nullable": true + }, + "usernameClaim": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider claim type to use for username." + }, + "fullNameClaim": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Provider claim type to use for Full name." + }, + "issuerName": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "serviceProviderName": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "singleSignOnServiceUrl": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "format": "uri" + }, + "singleLogoutServiceUrl": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "format": "uri" + }, + "certificates": { + "minItems": 1, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "ServiceAccount": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.", + "format": "int32", + "readOnly": true + }, + "createdBy": { + "$ref": "#/components/schemas/UserLink" + }, + "modifiedBy": { + "$ref": "#/components/schemas/UserLink" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "clientId": { + "maxLength": 256, + "type": "string", + "nullable": true + }, + "lastLoginAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "readOnly": true + }, + "clientSecrets": { + "maxItems": 2, + "type": "array", + "items": { + "$ref": "#/components/schemas/UserClientSecret" + }, + "nullable": true, + "readOnly": true + }, + "accessControlEntry": { + "$ref": "#/components/schemas/ServiceAccountBasedAccessControlEntry" + } + }, + "additionalProperties": false + }, + "ServiceAccountBasedAccessControlEntry": { + "type": "object", + "properties": { + "apiResources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceLink" + }, + "nullable": true + }, + "apiResourceRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceRoleLink" + }, + "nullable": true + }, + "applications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationLink" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "SupportedCulture": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "SupportedCultures": { + "type": "object", + "properties": { + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportedCulture" + }, + "nullable": true, + "readOnly": true + }, + "regions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportedCulture" + }, + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "User": { + "required": [ + "identityProvider", + "identityProviderKey", + "name", + "subject" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.", + "format": "int32", + "readOnly": true + }, + "createdBy": { + "$ref": "#/components/schemas/UserLink" + }, + "modifiedBy": { + "$ref": "#/components/schemas/UserLink" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "email": { + "maxLength": 256, + "type": "string", + "format": "email", + "nullable": true + }, + "language": { + "maxLength": 20, + "type": "string", + "nullable": true + }, + "region": { + "maxLength": 20, + "type": "string", + "nullable": true + }, + "subject": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Readonly on Update" + }, + "identityProvider": { + "$ref": "#/components/schemas/IdentityProviderLink" + }, + "identityProviderKey": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "Readonly on Update" + }, + "clientId": { + "maxLength": 256, + "type": "string", + "description": "Readonly on Update", + "nullable": true, + "readOnly": true + }, + "isEnabled": { + "type": "boolean", + "default": true + }, + "lastLoginAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "readOnly": true + }, + "clientSecrets": { + "maxItems": 2, + "type": "array", + "items": { + "$ref": "#/components/schemas/UserClientSecret" + }, + "nullable": true, + "readOnly": true + }, + "accessControlEntry": { + "$ref": "#/components/schemas/UserBasedAccessControlEntry" + } + }, + "additionalProperties": false + }, + "UserBasedAccessControlEntry": { + "type": "object", + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationLink" + }, + "nullable": true + }, + "apiResources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceLink" + }, + "nullable": true + }, + "apiResourceRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiResourceRoleLink" + }, + "nullable": true + } + }, + "additionalProperties": false, + "description": "Represents user access control for applications , resources and roles." + }, + "UserClientSecret": { + "required": [ + "clientSecret", + "expiresAt" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Autogenerated Id.", + "format": "int32", + "readOnly": true + }, + "clientSecret": { + "maxLength": 4000, + "minLength": 1, + "type": "string", + "readOnly": true + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "lastLoginAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, + "UserInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "subject": { + "type": "string", + "nullable": true + }, + "avatar": { + "type": "string", + "nullable": true + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "isAnonymous": { + "type": "boolean" + }, + "locale": { + "$ref": "#/components/schemas/UserLocaleInfo" + } + }, + "additionalProperties": false + }, + "UserLink": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false, + "readOnly": true + }, + "UserLocaleInfo": { + "type": "object", + "properties": { + "localeId": { + "type": "string", + "nullable": true + }, + "languageId": { + "type": "string", + "nullable": true + }, + "languageCode": { + "type": "string", + "nullable": true + }, + "fullDateTimeFormat": { + "type": "string", + "nullable": true + }, + "shortDateTimeFormat": { + "type": "string", + "nullable": true + }, + "longDateFormat": { + "type": "string", + "nullable": true + }, + "longTimeFormat": { + "type": "string", + "nullable": true + }, + "shortDateFormat": { + "type": "string", + "nullable": true + }, + "shortTimeFormat": { + "type": "string", + "nullable": true + }, + "amDesignator": { + "type": "string", + "nullable": true + }, + "pmDesignator": { + "type": "string", + "nullable": true + }, + "dayNames": { + "nullable": true + }, + "shortDayNames": { + "nullable": true + }, + "shortestDayNames": { + "nullable": true + }, + "firstDayOfWeek": { + "type": "string", + "nullable": true + }, + "monthNames": { + "nullable": true + }, + "shortMonthNames": { + "nullable": true + } + }, + "additionalProperties": false + }, + "WindowsParameters": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/IdentityProviderParameters" + } + ], + "additionalProperties": false + } + } + }, + "tags": [ + { + "name": "ApiResources", + "description": "API resources represent the services and roles that can be associated with an identity provider or user." + }, + { + "name": "Applications", + "description": "Applications that are secured by Access Management." + }, + { + "name": "AuditLogs", + "description": "Audit logs store information about every action taken in Access Management." + }, + { + "name": "IdentityProviders", + "description": "Identity providers allow you to log in with an account from an external system." + }, + { + "name": "ServiceAccounts", + "description": "Service accounts are used to run various services and applications." + }, + { + "name": "System", + "description": "System-level operations that are not directly related to specific entities." + }, + { + "name": "Users", + "description": "Non-service users within the system." + } + ] +} \ No newline at end of file diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj new file mode 100644 index 00000000..eaf9c353 --- /dev/null +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + + + + + + + + + NSwagCSharp + Trisoft.ISHRemote.OpenApiAM10 + OpenApiAM10{controller}Service + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 index a53d05c8..9d82f40a 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 @@ -614,7 +614,8 @@ Describe "New-IshSession" -Tags "Read" { } It "IshSession.OpenApiISH30Service" { if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 - $localIShSession.OpenApiISH30Service | Should -BeNullOrEmpty + $json = $localIShSession.OpenApiISH30Service.GetApplicationVersionAsync() + $json.Result | Should -Be $ishSession.ServerVersion } } It "IshSession.Annotation25" { diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs index f7706a30..97d1c564 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs @@ -33,7 +33,6 @@ using IdentityModel.OidcClient; using System.Threading.Tasks; using System.Threading; -using Trisoft.ISHRemote.OpenApiISH30; using System.Security.Claims; using System.Text; using System.Xml; diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs b/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs index 23a72e31..ac78a527 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs @@ -599,7 +599,7 @@ public int ChunkSize set { _chunkSize = value; } } - #region OpenApi internal/3.0 Services + #region OpenApi Services public OpenApiISH30Service OpenApiISH30Service { get @@ -1087,6 +1087,12 @@ private void VerifyConnectionValidity() } break; case Enumerations.Protocol.WcfSoapWithOpenIdConnect: + if (_infoShareOpenApiWithOpenIdConnectConnection == null) + { + // ... discard OpenApiISH30Service + // ...and re-create connection + CreateOpenApiWithOpenIdConnectConnection(); + } if (_infoShareWcfSoapWithOpenIdConnectConnection == null) { // Not valid... diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj index cbbf259b..74948813 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj @@ -42,6 +42,7 @@ + From 851be40368306fed5d033daa11fd6a668e80f6d0 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Tue, 26 Mar 2024 20:34:36 +0100 Subject: [PATCH 02/14] #180 Add IShSession.OpenApiAM10Client proxy similar to IShSession.OpenApiISH30Client proxy... Proxy always enabled, also for protocol WcfSoapWithOpenIdConnect. Added Trisoft.ISHRemote.OpenApiAM10 based on released ISHAM 15.0.0... Still a PS51 System.ComponentModel.Annotations execution issue... Later update NuGet on all projects --- Source/ISHRemote/Directory.Build.props | 10 +-- .../OpenApiAM10.json | 78 +++++++++---------- .../Trisoft.ISHRemote.OpenApiAM10.csproj | 8 +- .../OpenApiISH30.json | 4 +- .../Trisoft.ISHRemote.OpenApiISH30.csproj | 8 +- .../Cmdlets/Folder/AddIshFolder.cs | 4 +- ...ShareOpenApiWithOpenIdConnectConnection.cs | 41 ++++++++-- .../Objects/Public/IshSession.cs | 15 +++- 8 files changed, 103 insertions(+), 65 deletions(-) diff --git a/Source/ISHRemote/Directory.Build.props b/Source/ISHRemote/Directory.Build.props index df11a417..2c1a7ad1 100644 --- a/Source/ISHRemote/Directory.Build.props +++ b/Source/ISHRemote/Directory.Build.props @@ -25,10 +25,10 @@ $(VersionMajor).$(VersionMinor).$(VersionPatch) $(VersionMajor).$(VersionMinor).$(VersionPatch) - RWS Group for and on behalf of its affiliates and subsidiaries - RWS Group for and on behalf of its affiliates and subsidiaries - RWS Group for and on behalf of its affiliates and subsidiaries - Copyright (c) $([System.DateTime]::Now.ToString('yyyy')) All Rights Reserved by the RWS Group for and on behalf of its affiliates and subsidiaries. + RWS Holdings plc including its subsidiaries and affiliated companies + RWS Holdings plc including its subsidiaries and affiliated companies + RWS Holdings plc including its subsidiaries and affiliated companies + Copyright (c) $([System.DateTime]::Now.ToString('yyyy')) RWS Holdings plc including its subsidiaries and affiliated companies. All rights reserved. @@ -44,7 +44,7 @@ $(ProductName) Tridion Docs Content Manager $(ProductName) Business automation module on top of Tridion Docs Content Manager (Knowledge Center Content Manager, LiveContent Architect, Trisoft InfoShare) - RWS Group for and on behalf of its affiliates and subsidiaries + RWS Holdings plc including its subsidiaries and affiliated companies Tridion Docs Content Manager $(ProductName) 0314ffdb-6083-4c4d-aa01-8bfaac51a7e4 5.1 diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json index 0e492916..5d2017f6 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json @@ -1,7 +1,7 @@ { "openapi": "3.0.1", "info": { - "title": "Access Management API", + "title": "Access Management API v1.0.0", "description": "Access Management provides implementers with a simplified approach to identity management and gives administrators a central location for ongoing management of access to applications. ", "version": "v1" }, @@ -17,7 +17,7 @@ "ApiResources" ], "summary": "Gets a list of all API resources in the system.", - "operationId": "ApiResources_Get", + "operationId": "ApiResourcesGet", "responses": { "200": { "description": "Returns a list of API resources.", @@ -61,7 +61,7 @@ "ApiResources" ], "summary": "Gets the API resource with the given ID.", - "operationId": "ApiResources_GetById", + "operationId": "ApiResourcesGetById", "parameters": [ { "name": "id", @@ -124,7 +124,7 @@ "Applications" ], "summary": "Gets a list of all applications in the system.", - "operationId": "Applications_Get", + "operationId": "ApplicationsGet", "responses": { "200": { "description": "Returns a list of applications.", @@ -166,7 +166,7 @@ "Applications" ], "summary": "Creates a new application.", - "operationId": "Applications_Create", + "operationId": "ApplicationsCreate", "requestBody": { "description": "The application to create.", "content": { @@ -232,7 +232,7 @@ "Applications" ], "summary": "Gets the application with the given ID.", - "operationId": "Applications_GetById", + "operationId": "ApplicationsGetById", "parameters": [ { "name": "id", @@ -293,7 +293,7 @@ "Applications" ], "summary": "Updates an existing application.", - "operationId": "Applications_Update", + "operationId": "ApplicationsUpdate", "parameters": [ { "name": "id", @@ -379,7 +379,7 @@ "Applications" ], "summary": "Permanently deletes an existing application.", - "operationId": "Applications_Delete", + "operationId": "ApplicationsDelete", "parameters": [ { "name": "id", @@ -435,7 +435,7 @@ "AuditLogs" ], "summary": "Gets a list of audit log entries matching the given filter.", - "operationId": "AuditLogs_GetByFilter", + "operationId": "AuditLogsGetByFilter", "parameters": [ { "name": "UserName", @@ -601,7 +601,7 @@ "AuditLogs" ], "summary": "Gets the audit log entry with the given ID.", - "operationId": "AuditLogs_GetById", + "operationId": "AuditLogsGetById", "parameters": [ { "name": "id", @@ -704,7 +704,7 @@ "IdentityProviders" ], "summary": "Gets a list of available identity provider types.", - "operationId": "IdentityProviders_GetAvailableIdpTypes", + "operationId": "IdentityProvidersGetAvailableIdpTypes", "responses": { "200": { "description": "Returns the list of available identity provider types.", @@ -738,7 +738,7 @@ "IdentityProviders" ], "summary": "Gets a list of all identity providers in the system.", - "operationId": "IdentityProviders_Get", + "operationId": "IdentityProvidersGet", "responses": { "200": { "description": "Returns a list of identity providers.", @@ -780,7 +780,7 @@ "IdentityProviders" ], "summary": "Creates a new identity provider.", - "operationId": "IdentityProviders_Create", + "operationId": "IdentityProvidersCreate", "requestBody": { "description": "The identity provider to create.", "content": { @@ -846,7 +846,7 @@ "IdentityProviders" ], "summary": "Gets the identity provider with the given ID.", - "operationId": "IdentityProviders_GetById", + "operationId": "IdentityProvidersGetById", "parameters": [ { "name": "id", @@ -907,7 +907,7 @@ "IdentityProviders" ], "summary": "Updates an existing identity provider.", - "operationId": "IdentityProviders_Update", + "operationId": "IdentityProvidersUpdate", "parameters": [ { "name": "id", @@ -993,7 +993,7 @@ "IdentityProviders" ], "summary": "Permanently deletes an existing identity provider.", - "operationId": "IdentityProviders_Delete", + "operationId": "IdentityProvidersDelete", "parameters": [ { "name": "id", @@ -1050,7 +1050,7 @@ ], "summary": "Gets the icon for a given identity provider.", "description": "If no icon has been specified for the identity provider, a default icon is returned.", - "operationId": "IdentityProviders_GetIcon", + "operationId": "IdentityProvidersGetIcon", "parameters": [ { "name": "name", @@ -1081,7 +1081,7 @@ "IdentityProviders" ], "summary": "Gets a list of all login possibilities currently available.", - "operationId": "IdentityProviders_GetLoginOptions", + "operationId": "IdentityProvidersGetLoginOptions", "responses": { "200": { "description": "Returns the list of login options.", @@ -1125,7 +1125,7 @@ "IdentityProviders" ], "summary": "Gets a list of parameters specific to a given type of identity provider.", - "operationId": "IdentityProviders_GetParametersForIdentityProviderType", + "operationId": "IdentityProvidersGetParametersForIdentityProviderType", "parameters": [ { "name": "providerType", @@ -1187,7 +1187,7 @@ "ServiceAccounts" ], "summary": "Gets a list of all service accounts in the system.", - "operationId": "ServiceAccounts_Get", + "operationId": "ServiceAccountsGet", "responses": { "200": { "description": "Returns a list of service accounts.", @@ -1229,7 +1229,7 @@ "ServiceAccounts" ], "summary": "Creates a new service account.", - "operationId": "ServiceAccounts_Create", + "operationId": "ServiceAccountsCreate", "requestBody": { "description": "The data of the service account to create.", "content": { @@ -1295,7 +1295,7 @@ "ServiceAccounts" ], "summary": "Gets the service account with the given ID.", - "operationId": "ServiceAccounts_GetById", + "operationId": "ServiceAccountsGetById", "parameters": [ { "name": "id", @@ -1356,7 +1356,7 @@ "ServiceAccounts" ], "summary": "Updates an existing service account.", - "operationId": "ServiceAccounts_Update", + "operationId": "ServiceAccountsUpdate", "parameters": [ { "name": "id", @@ -1442,7 +1442,7 @@ "ServiceAccounts" ], "summary": "Permanently deletes an existing service account.", - "operationId": "ServiceAccounts_Delete", + "operationId": "ServiceAccountsDelete", "parameters": [ { "name": "id", @@ -1498,7 +1498,7 @@ "ServiceAccounts" ], "summary": "Generates a new client secret that can be used to log in as a given service account.", - "operationId": "ServiceAccounts_GenerateClientSecret", + "operationId": "ServiceAccountsGenerateClientSecret", "parameters": [ { "name": "serviceAccountId", @@ -1561,7 +1561,7 @@ "ServiceAccounts" ], "summary": "Updates an existing client secret.", - "operationId": "ServiceAccounts_UpdateClientSecret", + "operationId": "ServiceAccountsUpdateClientSecret", "parameters": [ { "name": "serviceAccountId", @@ -1657,7 +1657,7 @@ "ServiceAccounts" ], "summary": "Permanently deletes an existing client secret from a service account.", - "operationId": "ServiceAccounts_DeleteClientSecret", + "operationId": "ServiceAccountsDeleteClientSecret", "parameters": [ { "name": "serviceAccountId", @@ -1723,7 +1723,7 @@ "System" ], "summary": "Gets the login status of the current user.", - "operationId": "System_GetLoginStatus", + "operationId": "SystemGetLoginStatus", "responses": { "200": { "description": "Returns either None, LoggedIn, or Anonymous (if there are no IDPs yet or the configuration forces an anonymous state).", @@ -1774,7 +1774,7 @@ "System" ], "summary": "Gets information about the current user.", - "operationId": "System_GetUserInfo", + "operationId": "SystemGetUserInfo", "responses": { "200": { "description": "Returns information about the current user.", @@ -1825,7 +1825,7 @@ "System" ], "summary": "Gets a list of all supported languages and a list of all supported regions.", - "operationId": "System_GetSupportedCultures", + "operationId": "SystemGetSupportedCultures", "responses": { "200": { "description": "Returns an object with a list of all supported languages and a separate list of supported regions.", @@ -1876,7 +1876,7 @@ "Users" ], "summary": "Gets a list of all users in the system.", - "operationId": "Users_Get", + "operationId": "UsersGet", "responses": { "200": { "description": "Returns a list of users.", @@ -1920,7 +1920,7 @@ "Users" ], "summary": "Gets the user with the given ID.", - "operationId": "Users_GetById", + "operationId": "UsersGetById", "parameters": [ { "name": "id", @@ -1981,7 +1981,7 @@ "Users" ], "summary": "Updates an existing user.", - "operationId": "Users_Update", + "operationId": "UsersUpdate", "parameters": [ { "name": "id", @@ -2068,7 +2068,7 @@ ], "summary": "Permanently deletes an existing user.", "description": "Note that deleted users will automatically be recreated if they log into Access Management afterwards.\r\nTo completely remove users, first remove them from the external identity provider.", - "operationId": "Users_Delete", + "operationId": "UsersDelete", "parameters": [ { "name": "id", @@ -2124,7 +2124,7 @@ "Users" ], "summary": "Deactivates a user, so they can no longer log in.", - "operationId": "Users_DeactivateUser", + "operationId": "UsersDeactivateUser", "parameters": [ { "name": "id", @@ -2170,7 +2170,7 @@ "Users" ], "summary": "Re-activates a deactivated user so they can log in again.", - "operationId": "Users_ActivateUser", + "operationId": "UsersActivateUser", "parameters": [ { "name": "id", @@ -2216,7 +2216,7 @@ "Users" ], "summary": "Generates a new client secret that can be used to log in as a given user.\r\n(Client secrets for users are deprecated. Please use service accounts instead.)", - "operationId": "Users_GenerateClientSecret", + "operationId": "UsersGenerateClientSecret", "parameters": [ { "name": "userId", @@ -2280,7 +2280,7 @@ "Users" ], "summary": "Updates an existing client secret.\r\n(Client secrets for users are deprecated. Please use service accounts instead.)", - "operationId": "Users_UpdateClientSecret", + "operationId": "UsersUpdateClientSecret", "parameters": [ { "name": "userId", @@ -2377,7 +2377,7 @@ "Users" ], "summary": "Permanently deletes an existing client secret from a user.\r\n(Client secrets for users are deprecated. Please use service accounts instead.)", - "operationId": "Users_DeleteClientSecret", + "operationId": "UsersDeleteClientSecret", "parameters": [ { "name": "userId", diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj index eaf9c353..c9b306ef 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj @@ -12,17 +12,19 @@ NSwagCSharp Trisoft.ISHRemote.OpenApiAM10 - OpenApiAM10{controller}Service + OpenApiAM10{controller}Client + /ExceptionClass:OpenApiAM10{controller}Exception /OperationGenerationMode:SingleClientFromOperationId /GenerateClientClasses:true /UseBaseUrl:true /GenerateBaseUrlProperty:true /UseHttpClientCreationMethod:true + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json index e91e204f..98da3b10 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json @@ -1,7 +1,7 @@ { "openapi": "3.0.1", "info": { - "title": "InfoShare API v3.0", + "title": "InfoShare API v3.0.0", "contact": { "name": "RWS Support", "url": "https://gateway.sdl.com/" @@ -10,7 +10,7 @@ }, "servers": [ { - "url": "https://ish.example.com/ISHCS/OrganizeSpace/OApi" + "url": "https://ish.example.com/ISHWS/Api" } ], "paths": { diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj index c7daec35..5b50ac0f 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj @@ -12,17 +12,19 @@ NSwagCSharp Trisoft.ISHRemote.OpenApiISH30 - OpenApiISH30{controller}Service + OpenApiISH30{controller}Client + /ExceptionClass:OpenApiISH30{controller}Exception /OperationGenerationMode:SingleClientFromOperationId /GenerateClientClasses:true /UseBaseUrl:true /GenerateBaseUrlProperty:true /UseHttpClientCreationMethod:true + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/AddIshFolder.cs b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/AddIshFolder.cs index aef02917..18ddf221 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/AddIshFolder.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/AddIshFolder.cs @@ -206,7 +206,7 @@ protected override void ProcessRecord() } fieldValues.Add(fieldWriteAccess); - var response = IshSession.OpenApiISH30Service.CreateFolderAsync(new OpenApiISH30.CreateFolder() + var response = IshSession.OpenApiISH30Client.CreateFolderAsync(new OpenApiISH30.CreateFolder() { ParentId = ParentFolderId.ToString(), // TODO [Question] Why is folder id a string and not typed as long in the CreateFolder model? BaseObject is string, so exceptional folder long is string. Fields = fieldValues @@ -370,7 +370,7 @@ protected override void ProcessRecord() { ThrowTerminatingError(new ErrorRecord(trisoftAutomationException, base.GetType().Name, ErrorCategory.InvalidOperation, null)); } - catch (OpenApiISH30.ApiException exception) + catch (OpenApiISH30.OpenApiISH30Exception exception) { if (exception.Result != null) { diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs index 3a4836ce..f5ab4f89 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs @@ -27,6 +27,7 @@ using IdentityModel.OidcClient; using Trisoft.ISHRemote.Interfaces; using Trisoft.ISHRemote.OpenApiISH30; +using Trisoft.ISHRemote.OpenApiAM10; using System.Diagnostics; using System.Runtime.InteropServices; using IdentityModel.OidcClient.Infrastructure; @@ -36,10 +37,16 @@ namespace Trisoft.ISHRemote.Connection internal sealed class InfoShareOpenApiWithOpenIdConnectConnection : InfoShareOpenIdConnectConnectionBase, IDisposable { #region Private Members + + + /// + /// OpenApi Access Management Web Service v1.0 NSwag generated client + /// + private readonly OpenApiAM10Client _openApiAM10Client; /// /// OpenApi InfoShare Web Service v3.0 NSwag generated client /// - private OpenApiISH30Service _openApiISH30Service; + private readonly OpenApiISH30Client _openApiISH30Client; /// /// Tracking standard dispose pattern /// @@ -87,9 +94,12 @@ public InfoShareOpenApiWithOpenIdConnectConnection(ILogger logger, HttpClient ht // Don't think this will happen _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection reusing AccessToken[{ _connectionParameters.Tokens.AccessToken}] AccessTokenExpiration[{ _connectionParameters.Tokens.AccessTokenExpiration}]"); } - _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection using Normalized infoShareWSBaseUri[{infoShareWSUrlForOpenApi}]"); - _openApiISH30Service = new Trisoft.ISHRemote.OpenApiISH30.OpenApiISH30Service(_httpClient); - _openApiISH30Service.BaseUrl = new Uri(infoShareWSUrlForOpenApi, "api").ToString(); + _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection OpenApiISH30Client using infoShareWSBaseUri[{infoShareWSUrlForOpenApi}]"); + _openApiISH30Client = new OpenApiISH30Client(_httpClient); + _openApiISH30Client.BaseUrl = new Uri(infoShareWSUrlForOpenApi, "api").ToString(); + _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection OpenApiAM10Client using IssuerUrl[{_connectionParameters.IssuerUrl}]"); + _openApiAM10Client = new OpenApiAM10Client(_httpClient); + _openApiAM10Client.BaseUrl = _connectionParameters.IssuerUrl.ToString(); } #endregion @@ -100,10 +110,21 @@ public InfoShareOpenApiWithOpenIdConnectConnection(ILogger logger, HttpClient ht /// This method wraps the token up in an authentication/bearer token. /// /// The proxy - public OpenApiISH30Service GetOpenApiISH30ServiceProxy() + public OpenApiISH30Client GetOpenApiISH30Client() { _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GetAccessToken()); - return _openApiISH30Service; + return _openApiISH30Client; + } + /// + /// Create an OpenAPI Access Management 1.0 proxy + /// HttpClient with OpenIdConnect authentication need a way to pass the Access/Bearer token. + /// This method wraps the token up in an authentication/bearer token. + /// + /// The proxy + public OpenApiAM10Client GetOpenApiAM10Client() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GetAccessToken()); + return _openApiAM10Client; } #endregion @@ -114,9 +135,13 @@ private void Dispose(bool disposing) { if (disposing) { - if (_openApiISH30Service != null) + if (_openApiISH30Client != null) + { + ((IDisposable)_openApiISH30Client).Dispose(); + } + if (_openApiAM10Client != null) { - ((IDisposable)_openApiISH30Service).Dispose(); + ((IDisposable)_openApiAM10Client).Dispose(); } } disposedValue = true; diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs b/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs index ac78a527..eabdf97b 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs @@ -24,6 +24,7 @@ using Trisoft.ISHRemote.Connection; using Trisoft.ISHRemote.HelperClasses; using Trisoft.ISHRemote.Interfaces; +using Trisoft.ISHRemote.OpenApiAM10; using Trisoft.ISHRemote.OpenApiISH30; namespace Trisoft.ISHRemote.Objects.Public @@ -274,7 +275,7 @@ private void CreateOpenApiWithOpenIdConnectConnection() _logger.WriteVerbose($"CreateOpenApiWithOpenIdConnectConnection"); _infoShareOpenApiWithOpenIdConnectConnection = new InfoShareOpenApiWithOpenIdConnectConnection(_logger, _httpClient, _infoShareOpenIdConnectConnectionParameters); _logger.WriteDebug("CreateOpenApiWithOpenIdConnectConnection openApi30Service.GetApplicationVersionAsync"); - _serverVersion = new IshVersion(_infoShareOpenApiWithOpenIdConnectConnection.GetOpenApiISH30ServiceProxy().GetApplicationVersionAsync().GetAwaiter().GetResult()); + _serverVersion = new IshVersion(_infoShareOpenApiWithOpenIdConnectConnection.GetOpenApiISH30Client().GetApplicationVersionAsync().GetAwaiter().GetResult()); } internal IshTypeFieldSetup IshTypeFieldSetup @@ -600,12 +601,20 @@ public int ChunkSize } #region OpenApi Services - public OpenApiISH30Service OpenApiISH30Service + public OpenApiISH30Client OpenApiISH30Client { get { VerifyConnectionValidity(); - return _infoShareOpenApiWithOpenIdConnectConnection.GetOpenApiISH30ServiceProxy(); + return _infoShareOpenApiWithOpenIdConnectConnection.GetOpenApiISH30Client(); + } + } + public OpenApiAM10Client OpenApiAM10Client + { + get + { + VerifyConnectionValidity(); + return _infoShareOpenApiWithOpenIdConnectConnection.GetOpenApiAM10Client(); } } #endregion From c27b81c4daa9c3920601f5b5a04a844398fb8a56 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Thu, 4 Apr 2024 21:39:53 +0200 Subject: [PATCH 03/14] #180 Add IShSession.OpenApiAM10Client proxy similar to IShSession.OpenApiISH30Client proxy... PS51 System.ComponentModel.Annotations execution issue, extend partial OpenApi*Client with forced assembly load... Later update NuGet on all projects --- Doc/TheExecution-ISHRemote-8.0.md | 1 + Source/ISHRemote/ISHRemote.sln | 35 ++++++++++++------- .../OpenApiAM10.json | 4 +-- ...risoft.ISHRemote.OpenApiAM10.NET48.csproj} | 2 +- ...Trisoft.ISHRemote.OpenApiAM10.NET60.csproj | 34 ++++++++++++++++++ .../OpenApiISH30.json | 4 +-- ...isoft.ISHRemote.OpenApiISH30.NET48.csproj} | 2 +- ...risoft.ISHRemote.OpenApiISH30.NET60.csproj | 34 ++++++++++++++++++ .../Cmdlets/Folder/GetIshFolder.cs | 16 +++++++++ .../AppDomainAssemblyResolveHelper.cs | 7 +++- .../Trisoft.ISHRemote.csproj | 17 ++++++--- 11 files changed, 132 insertions(+), 24 deletions(-) rename Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/{Trisoft.ISHRemote.OpenApiAM10.csproj => Trisoft.ISHRemote.OpenApiAM10.NET48.csproj} (97%) create mode 100644 Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj rename Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/{Trisoft.ISHRemote.OpenApiISH30.csproj => Trisoft.ISHRemote.OpenApiISH30.NET48.csproj} (97%) create mode 100644 Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj diff --git a/Doc/TheExecution-ISHRemote-8.0.md b/Doc/TheExecution-ISHRemote-8.0.md index 72b3f68b..0b63f98a 100644 --- a/Doc/TheExecution-ISHRemote-8.0.md +++ b/Doc/TheExecution-ISHRemote-8.0.md @@ -202,6 +202,7 @@ For whoever stumbles on this transitive package dependency of `System.Runtime.Co * * Authentication over either Client Credentials or System Browser was succesful but the Bearer Token expired, the Refresh . Please create a `New-IShSession`. ... Every cmdlet will re-authenticate. # Expedite ISHRemote v8 - Must Have Section +* Again NET48 versus NET6.0 assembly reference issues, this time `System.ComponentModel.Annotations` at runtime in NET48 calling AM10 get users... considering making these assist library multi-target to tune the dependencies instead of standard2.0 ... but csproj `ProjectReference` within a solution cannot handle multi-target... so consider local nuget build, see https://weblog.west-wind.com/posts/2022/Sep/11/Referencing-a-Local-Private-NuGet-Package-in-your-Solution * Validate unhappy paths by manual and automated testing. * Authentication over Client Credentials Flow with non-existing `-ClientId` will . Please make sure you activate a client/secret on your Access Management User Profile (ISHAM). * Authentication over Client Credentials Flow with expired `-ClientId`/`-ClientSecret` combination will . Please recycle expired client/secret on your Access Management User Profile (ISHAM). diff --git a/Source/ISHRemote/ISHRemote.sln b/Source/ISHRemote/ISHRemote.sln index 3aaea3ba..0b5a2978 100644 --- a/Source/ISHRemote/ISHRemote.sln +++ b/Source/ISHRemote/ISHRemote.sln @@ -4,13 +4,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.0.32802.463 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote", "Trisoft.ISHRemote\Trisoft.ISHRemote.csproj", "{A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}" - ProjectSection(ProjectDependencies) = postProject - {14F8CA44-6ACF-442B-B436-33D829B4CB83} = {14F8CA44-6ACF-442B-B436-33D829B4CB83} - EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.csproj", "{E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10.NET60", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj", "{98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.csproj", "{14F8CA44-6ACF-442B-B436-33D829B4CB83}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30.NET48", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET48.csproj", "{64DD5938-82FE-4486-B6EC-9D8271715ED5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30.NET60", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET60.csproj", "{247EF7C4-018C-4972-BC8C-A9B595A59EB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10.NET48", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET48.csproj", "{896F5D8D-D254-4796-80BA-DA3F647DCC74}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,14 +23,22 @@ Global {A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}.Debug|Any CPU.Build.0 = Debug|Any CPU {A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}.Release|Any CPU.ActiveCfg = Release|Any CPU {A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}.Release|Any CPU.Build.0 = Release|Any CPU - {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E7C61499-DDD9-4DBC-B26D-0FE753D8BAAF}.Release|Any CPU.Build.0 = Release|Any CPU - {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14F8CA44-6ACF-442B-B436-33D829B4CB83}.Release|Any CPU.Build.0 = Release|Any CPU + {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Release|Any CPU.Build.0 = Release|Any CPU + {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Release|Any CPU.Build.0 = Release|Any CPU + {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Release|Any CPU.Build.0 = Release|Any CPU + {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json index 5d2017f6..99c17d7e 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/OpenApiAM10.json @@ -1,7 +1,7 @@ { "openapi": "3.0.1", "info": { - "title": "Access Management API v1.0.0", + "title": "Access Management API v1.0.0 (20240404)", "description": "Access Management provides implementers with a simplified approach to identity management and gives administrators a central location for ongoing management of access to applications. ", "version": "v1" }, @@ -3659,4 +3659,4 @@ "description": "Non-service users within the system." } ] -} \ No newline at end of file +} diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET48.csproj similarity index 97% rename from Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj rename to Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET48.csproj index c9b306ef..f797ee94 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET48.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj new file mode 100644 index 00000000..f797ee94 --- /dev/null +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj @@ -0,0 +1,34 @@ + + + + netstandard2.0 + + + + + + + + + NSwagCSharp + Trisoft.ISHRemote.OpenApiAM10 + OpenApiAM10{controller}Client + /ExceptionClass:OpenApiAM10{controller}Exception /OperationGenerationMode:SingleClientFromOperationId /GenerateClientClasses:true /UseBaseUrl:true /GenerateBaseUrlProperty:true /UseHttpClientCreationMethod:true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json index 98da3b10..ae950414 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/OpenApiISH30.json @@ -1,7 +1,7 @@ { "openapi": "3.0.1", "info": { - "title": "InfoShare API v3.0.0", + "title": "InfoShare API v3.0.0 (20240404)", "contact": { "name": "RWS Support", "url": "https://gateway.sdl.com/" @@ -14951,4 +14951,4 @@ "Bearer": [] } ] -} \ No newline at end of file +} diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET48.csproj similarity index 97% rename from Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj rename to Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET48.csproj index 5b50ac0f..7b4bb639 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET48.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj new file mode 100644 index 00000000..7b4bb639 --- /dev/null +++ b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj @@ -0,0 +1,34 @@ + + + + netstandard2.0 + + + + + + + + + NSwagCSharp + Trisoft.ISHRemote.OpenApiISH30 + OpenApiISH30{controller}Client + /ExceptionClass:OpenApiISH30{controller}Exception /OperationGenerationMode:SingleClientFromOperationId /GenerateClientClasses:true /UseBaseUrl:true /GenerateBaseUrlProperty:true /UseHttpClientCreationMethod:true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/GetIshFolder.cs b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/GetIshFolder.cs index 204c5db3..fb6c93aa 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/GetIshFolder.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Folder/GetIshFolder.cs @@ -69,6 +69,22 @@ namespace Trisoft.ISHRemote.Cmdlets.Folder /// /// Get folders recursively with filtering on folder type /// + /// + /// + /// New-IshSession -WsBaseUrl "https://example.com/ISHWS/" + /// $imageCount = 0 + /// $xmlCount = 0 + /// Get-IshFolder -FolderPath "General\Myfolder" -FolderTypeFilter @("ISHIllustration", "ISHModule", "ISHMasterDoc", "ISHLibrary") -Recurse | + /// Get-IshFolderContent -VersionFilter "" | + /// ForEach-Object -Process { + /// if ($_.IshType -in @("ISHIllustration")) { ++$imageCount } + /// if ($_.IshType -in @("ISHModule", "ISHMasterDoc", "ISHLibrary")) { ++$xmlCount } + /// } + /// Write-Host ("imageCount["+$imageCount+"]") + /// Write-Host ("xmlCount["+$xmlCount+"]") + /// + /// Various statistics can be gathered by crawling across many API calls. This sample recursively goes over some subfolder, and retrieves all content objects in the folder and aggregates to a rough count. The ForEach-Object construct is important as it only keeps the essence, the counters, and avoids keeping all objects retrieved over the API in memory - potentially running out of client-side/PowerShell memory. + /// [Cmdlet(VerbsCommon.Get, "IshFolder", SupportsShouldProcess = false)] [OutputType(typeof(IshFolder))] public sealed class GetIshFolder : FolderCmdlet diff --git a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs index 45c6aa46..919aa0f0 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014 All Rights Reserved by the SDL Group. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,6 +65,7 @@ internal static class AppDomainAssemblyResolveHelper /// * System.Text.Json requested 5.0.0.0 but we now return 5.0.0.2 /// * IdentityModel.OidcClient requested but we now return /// * Microsoft.Bcl.AsyncInterfaces requested 5.0.0.0 but we now return 6.0.0.0 + /// * System.ComponentModel.Annotations requested 4.2.0.0 but we now return 5.0.0.0 /// internal static void Redirect() { @@ -90,6 +91,10 @@ internal static void Redirect() var assemblyMBclAsyncInterfaces = Assembly.LoadFrom(filePathMBclAsyncInterfaces); _forcedLoadedAssemblies.GetOrAdd("Microsoft.Bcl.AsyncInterfaces", assemblyMBclAsyncInterfaces); + string filePathSCMAnnotations = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"System.ComponentModel.Annotations.dll"); + var assemblySCMAnnotations = Assembly.LoadFrom(filePathSCMAnnotations); + _forcedLoadedAssemblies.GetOrAdd("System.ComponentModel.Annotations", assemblySCMAnnotations); + _isAppDomainAssemblyResolveHelperRegistered = true; } } diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj index 74948813..7e7d230c 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj @@ -1,4 +1,4 @@ - + net48;net6.0 @@ -41,9 +41,18 @@ - - - + + + + + + + + From acbc23abac972e9372a2dbe39b3b83993a91fc19 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Fri, 5 Apr 2024 17:33:12 +0200 Subject: [PATCH 04/14] #180 Add IShSession.OpenApiAM10Client proxy similar to IShSession.OpenApiISH30Client proxy... PS51 System.ComponentModel.Annotations execution issue solved over AppDomainModuleAssemblyInitializer... Later update NuGet on all projects --- .../Cmdlets/Session/NewIshSession.Tests.ps1 | 29 +++++++++++++++---- .../AppDomainModuleAssemblyInitializer.cs | 9 ++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 index 9d82f40a..6eb26b6f 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/NewIshSession.Tests.ps1 @@ -538,9 +538,14 @@ Describe "New-IshSession" -Tags "Read" { BeforeAll { $localIShSession = New-IshSession -Protocol WcfSoapWithWsTrust -WsBaseUrl $webServicesBaseUrl -IshUserName $ishUserName -IshPassword $ishPassword } - It "IshSession.OpenApiISH30Service" { + It "IshSession.OpenApiISH30Client" { if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 - $localIShSession.OpenApiISH30Service | Should -BeNullOrEmpty + $localIShSession.OpenApiISH30Client | Should -BeNullOrEmpty + } + } + It "IshSession.OpenApiAM10Client" { + if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 + $localIShSession.OpenApiAM10Client | Should -BeNullOrEmpty } } It "IshSession.Annotation25" { @@ -612,12 +617,18 @@ Describe "New-IshSession" -Tags "Read" { $localIShSession = New-IshSession -Protocol WcfSoapWithOpenIdConnect -WsBaseUrl $webServicesBaseUrl -ClientId $amClientId -ClientSecret $amClientSecret } } - It "IshSession.OpenApiISH30Service" { + It "IshSession.OpenApiISH30Client" { if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 - $json = $localIShSession.OpenApiISH30Service.GetApplicationVersionAsync() + $json = $localIShSession.OpenApiISH30Client.GetApplicationVersionAsync() $json.Result | Should -Be $ishSession.ServerVersion } } + It "IshSession.OpenApiAM10Client" { + if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 + $json = $localIShSession.OpenApiAM10Client.IdentityProvidersGetAsync() + $json.Result.Count -ge 1 | Should -Be $true + } + } It "IshSession.Annotation25" { if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 $localIShSession.Annotation25 | Should -Not -BeNullOrEmpty @@ -721,12 +732,18 @@ Describe "New-IshSession" -Tags "Read" { $localIShSession = New-IshSession -Protocol OpenApiWithOpenIdConnect -WsBaseUrl $webServicesBaseUrl -ClientId $amClientId -ClientSecret $amClientSecret } } - It "IshSession.OpenApiISH30Service" { + It "IshSession.OpenApiISH30Client" { if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 - $json = $localIShSession.OpenApiISH30Service.GetApplicationVersionAsync() + $json = $localIShSession.OpenApiISH30Client.GetApplicationVersionAsync() $json.Result | Should -Be $ishSession.ServerVersion } } + It "IshSession.OpenApiAM10Client" { + if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 + $json = $localIShSession.OpenApiAM10Client.IdentityProvidersGetAsync() + $json.Result.Count -ge 1 | Should -Be $true + } + } It "IshSession.Annotation25" { if (([Version]$ishSession.ServerVersion).Major -ge 15) { # new service since 15/15.0.0 $localIShSession.Annotation25 | Should -Not -BeNullOrEmpty diff --git a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs index 1ea0108a..61baf8ca 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014 All Rights Reserved by the SDL Group. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,6 +50,7 @@ namespace Trisoft.ISHRemote.HelperClasses /// * System.Text.Json requested 5.0.0.0 but we now return 5.0.0.2 /// * IdentityModel.OidcClient requested but we now return /// * Microsoft.Bcl.AsyncInterfaces requested 5.0.0.0 but we now return 6.0.0.0 + /// * System.ComponentModel.Annotations requested 4.2.0.0 but we now return 4.2.1.0 (for NET48/OpenApi clients) /// /// Focus was to getting this working on NETFramework, more implementation is required to align with /// proposed solution of https://devblogs.microsoft.com/powershell/resolving-powershell-module-assembly-dependency-conflicts/ @@ -107,6 +108,10 @@ public void OnImport() filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"System.Memory.dll"); assembly = Assembly.LoadFrom(filePath); _forcedLoadedAssemblies.GetOrAdd("System.Memory", assembly); + + filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"System.ComponentModel.Annotations.dll"); + assembly = Assembly.LoadFrom(filePath); + _forcedLoadedAssemblies.GetOrAdd("System.ComponentModel.Annotations", assembly); #else AssemblyLoadContext.Default.Resolving += ResolveAssembly_NetCore; @@ -186,4 +191,4 @@ private static Assembly ResolveAssembly_NetCore( #endif } -} \ No newline at end of file +} From aa5d82a304cc339bd034f9e7874d1732aaabf637 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Fri, 5 Apr 2024 19:32:57 +0200 Subject: [PATCH 05/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Some basic Pester Tests... Later update NuGet on both projects --- .../_TestEnvironment/TestPrerequisite.Tests.ps1 | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 index 3491c66b..18d97a6d 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 @@ -34,15 +34,15 @@ Describe "Test-Prerequisite" -Tags "Read" { } } - Context "IshSession (<16.0.0) - Validating overwrites of ISHRemote.PesterSetup.Debug.ps1" { + Context "IshSession (-lt 16) - Validating overwrites of ISHRemote.PesterSetup.Debug.ps1" { BeforeAll { $ishSession = New-IshSession -Protocol WcfSoapWithWsTrust -WsBaseUrl $webServicesBaseUrl -IshUserName $ishUserName -IshPassword $ishPassword + $ishUser = Get-IshUser } It "IshSession.Protocol WcfSoapWithWsTrust" { $IshSession.Protocol | Should -Be 'WcfSoapWithWsTrust' } It "Current IShSession user should be part of VUSERGROUPSYSTEMMANAGEMENT UserGroup" { - $ishUser = Get-IshUser $ishUser.fusergroup_none_element -like "*VUSERGROUPSYSTEMMANAGEMENT*" | Should -Be $true } It "IshSession.AuthenticationContext" { @@ -62,7 +62,7 @@ Describe "Test-Prerequisite" -Tags "Read" { } } - Context "IshSession (=15.0.0) - Validating overwrites of ISHRemote.PesterSetup.Debug.ps1" { + Context "IshSession (-eq 15) - Validating overwrites of ISHRemote.PesterSetup.Debug.ps1" { BeforeAll { $ishSession = New-IshSession -WsBaseUrl $webServicesBaseUrl -ClientId $amClientId -ClientSecret $amClientSecret $ishUser = Get-IshUser @@ -72,6 +72,11 @@ Describe "Test-Prerequisite" -Tags "Read" { $IshSession.Protocol | Should -Be 'WcfSoapWithOpenIdConnect' } } + It "Current IShSession user over ClientId/ClientSecret should match UserName parameter so all tests run under the same account" { + if (([Version]$ishSession.ServerVersion).Major -eq 15) { + $ishUser.UserName | Should -Be $ishUserName + } + } It "Current IShSession user should be part of VUSERGROUPSYSTEMMANAGEMENT UserGroup" { if (([Version]$ishSession.ServerVersion).Major -eq 15) { $ishUser.fusergroup_none_element -like "*VUSERGROUPSYSTEMMANAGEMENT*" | Should -Be $true From aaddea0e2fe41e711659bbd64b8d637f7bbdf8c0 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Fri, 5 Apr 2024 21:27:09 +0200 Subject: [PATCH 06/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Force restore of Multi-target/Conditional ProjectReference in continuous-integration.yml... Later update NuGet on both projects --- .github/workflows/continuous-integration.yml | 9 ++- Doc/TheExecution-ISHRemote-8.0.md | 58 ++++++++++---------- README.MD | 16 +++++- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0ca5fcaa..b95abaa2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -48,8 +48,13 @@ jobs: with: dotnet-version: 6.0.x - - name: Restore project and dependencies - run: dotnet restore Source/ISHRemote/ISHRemote.sln + - name: Explicit restore of multi-target/conditional ProjectReference and dependencies + run: | + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET48.csproj + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET48.csproj + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET60.csproj + dotnet restore Source/ISHRemote/ISHRemote.sln - name: Build Solution run: dotnet build --no-restore --no-incremental --configuration release Source/ISHRemote/ISHRemote.sln diff --git a/Doc/TheExecution-ISHRemote-8.0.md b/Doc/TheExecution-ISHRemote-8.0.md index 0b63f98a..c3f5dab3 100644 --- a/Doc/TheExecution-ISHRemote-8.0.md +++ b/Doc/TheExecution-ISHRemote-8.0.md @@ -2,14 +2,10 @@ This page will try to track work in progress. And because I work on it in free time, it will help trace how I got where I am in the first place plus what is next. Inspired by [ThePlan-ISHRemote-7.0.md](./ThePlan-ISHRemote-7.0.md) and [TheExecution-ISHRemote-7.0.md](./TheExecution-ISHRemote-7.0.md). -Remember -* https://mecdev12qa01.global.sdl.corp/ISHWSSQL2017/Api/api-docs/index.html requires pre-authentication -* https://mecdev12qa01.global.sdl.corp/ISHCSSQL2017/OrganizeSpace/OApi/api-docs/index.html forces authentication - # On Tridion Docs 14SP4/14.0.4 and earlier -`New-IShSession` offered 3 parameter groups on Tridion Docs 14SP4/14.0.4 and earlier depending on the WS-Federation/WS-Trust configuration. That configuration is decided by `\InfoShareWS\connectionconfiguration.xml` for ISHRemote and repurposed from the Client Tools. +`New-IShSession` offered 3 parameter groups on Tridion Docs 14SP4/14.0.4 and earlier depending on the WS-Federation/WS-Trust configuration. That configuration is decided by `/InfoShareWS/connectionconfiguration.xml` for ISHRemote and repurposed from the Client Tools. * ActiveDirectory, so only `-WsBaseUrl`, where an empty/non-provided `-IShUserName` indicated fall back to `NetworkCredentials` which is also known as Windows Authentication. * PSCredential, so `-WsBaseUrl` and `-PSCredential`, where PowerShell will prompt for a username/password combination. * UserNamePassword, so `-WsBaseUrl`, `-IShUserName` and `-IShPassword`. The classic authentication on Tridion Docs User Profiles. @@ -44,9 +40,7 @@ Used on protocols `WcfSoapWithWsTrust` only for `UserNameMixed` variation; while - # Protocol and Parameter Group Scenarios - On Tridion Docs 14SPx/14.0.x and earlier, it is always `WcfSoapWithWsTrust`. Full functionality on PowerShell 5.1 regarding `WindowsMixed` and `UserNameMixed` while PowerShell 7.2+ is limited to authentication over `UserNameMixed` provided by `ISHSTS`-only. Starting from Tridion Docs 15/15.0.0 most customers will use `WcfSoapWithOpenIdConnect`. Legacy variation `WcfSoapWithWsTrust` can still be selected as well. Experimenting on Tridion Docs 15/15.0.0 is possible for cmdlets having a side-by-side implementation when using `Protocol` `OpenApiWithOpenIdConnect`; when the OpenAPI implementation is not there, a fall back to `WcfSoapWithOpenIdConnect` will happen. @@ -93,17 +87,16 @@ ISHRemote as an interactive user where you can actively provide credentials. Eve - # Problem: ISHRemote to combine ISHWS and ISHAM Data Sources -## Add public proxy OpenApiAM20Service next to OpenApiISH30Service -The `IshSession` object will need a public `OpenApiISH30Service` served by NSwag generated `Trisoft.ISHRemote.OpenApi.OpenApiISH30Service`. This proxy, just like pre-authenticated WCF SOAP WS-Trust proxies `DocumentObj25`, will be used by cmdlets but can be used to open functionality that is not supported by the cmdlets yet. +## Add public proxy OpenApiAM20Client next to OpenApiISH30Client +The `IshSession` object will need a public `OpenApiISH30Client` served by NSwag generated `Trisoft.ISHRemote.OpenApi.OpenApiISH30Client`. This proxy, just like pre-authenticated WCF SOAP WS-Trust proxies `DocumentObj25`, will be used by cmdlets but can be used to open functionality that is not supported by the cmdlets yet. -Imagine `IshSession` object offering a public `OpenApiAM20Service` served by NSwag generated `Trisoft.ISHRemote.OpenApi.OpenApiAM20Service`. This pre-authenticated proxy can open functionality that is not supported by cmdlets yet. Pre-authenticated as Tridion Docs User Profiles holding the **Administrator** User Role are administrator on Access Management (ISHAM) anyway. +Imagine `IshSession` object offering a public `OpenApiAM20Client` served by NSwag generated `Trisoft.ISHRemote.OpenApi.OpenApiAM20Client`. This pre-authenticated proxy can open functionality that is not supported by cmdlets yet. Pre-authenticated as Tridion Docs User Profiles holding the **Administrator** User Role are administrator on Access Management (ISHAM) anyway. -## IDEA: Add cmdlet Sync-IShUser +## IDEA: Add cmdlet Sync-IShUser [BACKLOG] By supporting `-WhatIf` one could see which changes will be applied either way, still returning objects that would get changed. By parameters similar to `Compare-IshTypeFieldDefinition` you could do left-hand vs right-hand side validation. ### User Profiles update from Tridion Docs to Access Management @@ -133,27 +126,31 @@ Typical cmdlet behavior - `Sync-IShUser -IShUser -ToAccessManagement Theoretical option, no user scenario yet. Perhaps there is Access Management Service user that - over *ClientID* does not have a match with a Tridion Docs User Profile - usage would result in a missing profile match error anyway. -## IDEA: Add IShSession Smart mode parameter to aggregate data sources +## IDEA: Add IShSession Smart mode parameter to aggregate data sources [BACKLOG] Would a **smart** mode on the session make sense? So imagine `Find-IShUser` or `Get-IShUser`... * Currently IShUser objects holds ISHWS information. So **LastLogin** is only filled in when the `PASSWORD` on this Tridion Docs User Profile was used, more and more scenarios over Access Management (ISHAM) will leave it empty. * Returning any ClientId and Secret (first characters) could help analysis. -## IDEA: Add Access Management cmdlets +## IDEA: Add Access Management cmdlets [BACKLOG] Either some basic cmdlets in **ISHRemote** that return an object model that can be used as input for other cmdlets. So `Get-AMUser` returns Access Management `AMUser`s, where the `ClientId` field could be used to `Find-IshUser`. And `Set-AMUser` accepting `IShUser` where the `FISHEXTERNALID` could be used to update Access Management user profiles. Add (nested binary module) AMRemote that could offer cmdlets like * `New-AMSession`, similar to `New-IShSession`, that returns an `AMSession` object with OpenApi proxy. * `Get-AMUser`, `Set-AMUser` and `Remove-AMUser`. It would be nice if ISHRemote and AMRemote would understand each others object model. That is why just adding some cmdlets in ISHRemote is so much easier than a clean AMRemote PowerShell automation library. + + # Compatiblity ISHRemote compatibility is on its Cmdlets and parameters groups wherever possible across ISHRemote major and minor versions. On the inside refactoring is required to introduce Wcf Soap with OpenIdConnect authentication or later even OpenApi with OpenIdConnect authentication. So in practice people that link to the ISHRemote assembly in their program code will find feature parity but might not find code compatibility. Refer to __ConnectionClassDiagram restructering but also example code. Explain the relations among these files, so WcfSoapBearerToken as workaround mentioned by Duende, delivering reused WcfSoap proxies that are compatible (which is cool!) + + # Done * Merging in #115 branch that was AsmxSoapWithAuthenticationContext plus OpenApiWithOpenIdConnect efforts * Update spec.json -* Rename protocol and ishSession.OpenApi30Service -> ishSession.OpenApiISH30Service so ishSession.OpenApiAM20Service +* Rename protocol and ishSession.OpenApi30Client -> ishSession.OpenApiISH30Client so ishSession.OpenApiAM20Client * Parameter group `New-IShSession` ActiveDirectory/Interactive does not have `-Timeout` parameter. * Cmdlets `New-IshSession` and `Test-IshSession` received parameter `-Protocol`, `-ClientId` and `-ClientSecret` so when protocol is set to `OpenApiWithOpenIdConnect` it is the preferred route, fall back to `WcfSoapWithWsTrust` when OpenApi calls are unavailable. * Case Files @@ -199,34 +196,35 @@ For whoever stumbles on this transitive package dependency of `System.Runtime.Co * Rolled back from Task.Run contruction to simply GetAwaiter().GetResult() as the latter allows logging to happen! * Using `New-IshSession` parameter `-PSCredential` on 14SP4/14.0.4 or earlier works like before, as it means username/password authentication over protocol `WcfSoapWithWsTrust`. However, using `-PSCredential` on 15/15.0.0 means that you are using protocol `WcfSoapOverOpenIdConnect`, so expecting a client/secret. If you then provide username/password, you will get error `GetTokensOverClientCredentialsAsync Access Error[invalid_client]`. Note that you can force by adding `-Protocol WcfSoapWithWsTrust` to the `New-IshSession` cmdlet. * Authentication over System Browser, so Authorization Code Flow with Proof Key for Code Exchange (PKCE), will give you 60 seconds. Any slower and you will see the `New-IShSession`/`Test-IShSession` cmdlets respond with `TaskCanceledException` exception stating `Browser login canceled after 60 seconds.` -* * Authentication over either Client Credentials or System Browser was succesful but the Bearer Token expired, the Refresh . Please create a `New-IShSession`. ... Every cmdlet will re-authenticate. +* Authentication over either Client Credentials or System Browser was succesful but the Bearer Token expired, the Refresh . Please create a `New-IShSession`. ... Every cmdlet will re-authenticate. +* Once branch #152 is merged, update ticket https://github.com/IdentityModel/Documentation/issues/13 with a hint to `AppDomainModuleAssemblyInitializer.cs` [POSTPONED] + > Took me a while to find this nugget to resolve my problem. It is unfortunate that `OidcClient` doesn't work without these assemblyBinding redirects. For people who have this issue but do not have access to a `.config` file like I had with `powershell.exe.config` (v5.1 on .NET 4.8) - have a look at `AppDomainModuleAssemblyInitializer.cs` on https://github.com/RWS/ISHRemote/ + > Another hint is adding `LogSerializer.Enabled = false;` because if you do not attach logging to OidcClient, there seemingly is a bug that still does logging although not configured. see https://github.com/IdentityModel/IdentityModel.OidcClient/pull/67 +* Again NET48 versus NET6.0 assembly reference issues, this time `System.ComponentModel.Annotations` at runtime in NET48 calling AM10 get users... considering making these assist library multi-target to tune the dependencies instead of standard2.0 failed ... but csproj `ProjectReference` within a solution cannot handle multi-target... so split each OpenApi generated assembly in a `NET48` and `NET60` variant so it allows `ISHRemote.csproj` conditional `ProjectReference` and allows to tune the NuGet library versions per platform. +* Validate unhappy paths by manual and automated testing. + * Authentication over Client Credentials Flow with non-existing `-ClientId` will . Please make sure you activate a client/secret on your Access Management User Profile (ISHAM). + * Authentication over Client Credentials Flow with expired `-ClientId`/`-ClientSecret` combination will . Please recycle expired client/secret on your Access Management User Profile (ISHAM). + * Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, but not mapped in the CMS to a User Profile over `FISHEXTERNALID` will . Please make sure that the client (which you can find on the Access Management User Profile) is added in Organize Space on one CMS User Profile in the comma-seperated External Id field. + * Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, and mapped in the CMS to a User Profile over `FISHEXTERNALID` which is disabled will . Please make sure in Organize Space that the one CMS User Profile holding the client in the External Id field is an enabled profile. + + # Expedite ISHRemote v8 - Must Have Section -* Again NET48 versus NET6.0 assembly reference issues, this time `System.ComponentModel.Annotations` at runtime in NET48 calling AM10 get users... considering making these assist library multi-target to tune the dependencies instead of standard2.0 ... but csproj `ProjectReference` within a solution cannot handle multi-target... so consider local nuget build, see https://weblog.west-wind.com/posts/2022/Sep/11/Referencing-a-Local-Private-NuGet-Package-in-your-Solution -* Validate unhappy paths by manual and automated testing. -* Authentication over Client Credentials Flow with non-existing `-ClientId` will . Please make sure you activate a client/secret on your Access Management User Profile (ISHAM). -* Authentication over Client Credentials Flow with expired `-ClientId`/`-ClientSecret` combination will . Please recycle expired client/secret on your Access Management User Profile (ISHAM). -* Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, but not mapped in the CMS to a User Profile over `FISHEXTERNALID` will . Please make sure that the client (which you can find on the Access Management User Profile) is added in Organize Space on one CMS User Profile in the comma-seperated External Id field. -* Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, and mapped in the CMS to a User Profile over `FISHEXTERNALID` which is disabled will . Please make sure in Organize Space that the one CMS User Profile holding the client in the External Id field is an enabled profile. * Help * $ishSessionA = New-IshSession -WsBaseUrl "https://example.com/ISHWSPROD/" -PSCredential "Admin" --> `-PSCredential Admin` only works for `-Protocol WcfSoapWithWsTrust` so it is an outdated sample ... all New-IshSession should be reviewed. # Next - Should Have Section +* Submit above [BACKLOG] entries as Github issues * Test refresh with short expiration - * $ishSession.OpenApiISH30Service.GetApplicationVersionAsync() results in `You cannot call a method on a null-valued expression.` + * $ishSession.OpenApiISH30Client.GetApplicationVersionAsync() results in `You cannot call a method on a null-valued expression.` * Get-IshVersion (over WcfSoapWithOpenIdConnect) results in `The HTTP status code of the response was not expected (401).` - - -* Extend perequisites test regarding client I'd and secret, an expired and valid set... Perhaps over isham20proxy +* Extend perequisites test regarding client I'd and secret, an expired and valid set... Perhaps over isham20proxy. Since 15.1 the ClientSecret expiration can be more than one year which in practice makes this test less important. * User provisioning, see [SRQ-23306] Last login date in user overview is not updated when authentication was done through an external identity provider - RWS Jira https://jira.sdl.com/browse/SRQ-23306 * ClientCredential Testing only `IShSession.RefreshTokenSkewTime`, requires `IShSession.AccessTokenExpirationDateTime` 60 min .... skew=5m .... 55m--> refresh ... Test... IShSession.SkewTime=59:50 (defaults to 5min) * Automated Test ps5.1 with wstrust, ps7 with both openidconnect * Test all protocol types on all platforms via newishsession (and one other smoke test) by calling it 6 times (2 ps times 3 protocols) which colors right after prerequisites -* Once branch #152 is merged, update ticket https://github.com/IdentityModel/Documentation/issues/13 with a hint to `AppDomainModuleAssemblyInitializer.cs` - > Took me a while to find this nugget to resolve my problem. It is unfortunate that `OidcClient` doesn't work without these assemblyBinding redirects. For people who have this issue but do not have access to a `.config` file like I had with `powershell.exe.config` (v5.1 on .NET 4.8) - have a look at `AppDomainModuleAssemblyInitializer.cs` on https://github.com/RWS/ISHRemote/ - > Another hint is adding `LogSerializer.Enabled = false;` because if you do not attach logging to OidcClient, there seemingly is a bug that still does logging although not configured. see https://github.com/IdentityModel/IdentityModel.OidcClient/pull/67 -* OpenAPI, add wires for streamed downloading of Get-IshPublicationOutputData (15.0.0 only, measure performance difference) +* OpenAPI, add wires for streamed downloading of Get-IshPublicationOutputData (15.0.0 only, measure performance difference) [BACKLOG] * OpenAPI, add wires for Folder cmdlets diff --git a/README.MD b/README.MD index b6ce2574..8bfd9c02 100644 --- a/README.MD +++ b/README.MD @@ -34,7 +34,7 @@ Have a look at the [Automating tasks in Tridion Docs using PowerShell](https://y # Install & Update -Below the shorthand, more details are on [Installation-ISHRemote-8.0.md](./Doc/Installation-ISHRemote-8.0.md). This link will guide you on package managers like `PowerShellGet` and newer `PSResourceGet`; cmdlets to install, update and uninstall. +Below the TLDR; more details are on [Installation-ISHRemote-8.0.md](./Doc/Installation-ISHRemote-8.0.md). This link will guide you on package managers like `PowerShellGet` and newer `PSResourceGet`; cmdlets to install, update and uninstall. ## Install on PowerShell 7.4+ (CoreCLR) powered by .NET 8.0+ @@ -62,6 +62,7 @@ Any feedback is welcome. Please log a GitHub issue, make sure you submit your ve # Known Issues & FAQ ## Execution Known Issues +* If you get `You cannot call a method on a null-valued expression.` or `The HTTP status code of the response was not expected (401).`, probably while using `$ishSession.OpenApiISH30Client` it means your token expired and requires a refresh. * If you get `New-IshSession : Reference to undeclared entity 'raquo'. Line 98, position 121.`, most likely you specified an unexisting "Web Services API" url. Make sure your url ends with an ending slash `/`. * If a test fails with `The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state.`, it probably means you didn't provide enough (mandatory) parameters to the WCF/SVC code so passing null parameters. Typically an `-IshPassword` is missing or using an existing username. @@ -105,7 +106,7 @@ In `ISHRemote.PesterSetup.Debug.ps1` override the global variables used for test ## Release Build and Publish To PowerShell Gallery Rought steps to release... -1. Use Visual Studio to build a `Debug` build. Run an `Invoke-Pester` (minimally v5.3.0) on `C:\GITHUB\ishremote\Source\ISHRemote\Trisoft.ISHRemote`. All steps should be green. +1. Use Visual Studio to build a `Debug` build. Run an `Invoke-Pester` (minimally Pester v5.3.0) on `C:\GITHUB\ishremote\Source\ISHRemote\Trisoft.ISHRemote`. All steps should be green. 1. Use Visual Studio to build a `Release` build. Change `C:\GITHUB\ISHRemote\Source\ISHRemote\Trisoft.ISHRemote\ISHRemote.PesterSetup.ps1` to forcefully load `\bin\release\` (instead of `\bin\debug\`). Run an `Invoke-Pester` on `C:\GITHUB\ishremote\Source\ISHRemote\Trisoft.ISHRemote`. All steps should be green. 1. The folder content of `C:\GITHUB\ISHRemote\Source\ISHRemote\Trisoft.ISHRemote\bin\Release\` will be published 1. Check all files, especially `Scripts` that you have all files. @@ -117,6 +118,17 @@ Rought steps to release... 1. Start new release notes, under a new version number like `v0.14-beta` 1. Close version milestone on https://github.com/sdl/ISHRemote/milestone/ +## Building Libraries in Visual Studio + +Since #180 where the `Trisoft.ISHRemote.csproj` multi-target/conditional ProjectReference was introduced, you might get a lot of Build errors looking like `error CS0234: The type or namespace name 'Reflection' does not exist in the namespace 'System' (are you missing an assembly reference?) [D:\a\ISHRemote\ISHRemote\Source\ISHRemote\Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj]` or `error CS0518: Predefined type 'System.String' is not defined or imported`. The simple answer is build again, it will eventually restore all project dependencies. +In [continuous-integration.yml](/.github/workflows/continuous-integration.yml) this issue was solved by explicit restores for the multi-target/conditional ProjectReferences. + + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET48.csproj + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET48.csproj + dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET60.csproj + dotnet restore Source/ISHRemote/ISHRemote.sln + ## Debugging PowerShell in Visual Studio Since Visual Studio 2022, we use `\ISHRemote\Trisoft.ISHRemote\Properties\launchSettings.json` which theoretically also works on Visual Studio Code. Don't forget to set the matching Framework for debugging the library, either `net48` or `net6.0`, for the matching PowerShell version in the start debugging dropdown. From 9b86ff3fb4b4fceadce0944440bdfa00f33a6cb1 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Sun, 7 Apr 2024 18:29:10 +0200 Subject: [PATCH 07/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Found a way back to netstandard2.0 OpenAPI libraries instead of Multi-target/Conditional ProjectReference... Later update NuGet on both projects --- .github/workflows/continuous-integration.yml | 11 +++--- Doc/ReleaseNotes-ISHRemote-8.0.md | 20 ++++++++--- Source/ISHRemote/ISHRemote.sln | 32 ++++++----------- ...Trisoft.ISHRemote.OpenApiAM10.NET60.csproj | 34 ------------------- ...j => Trisoft.ISHRemote.OpenApiAM10.csproj} | 0 ...risoft.ISHRemote.OpenApiISH30.NET60.csproj | 34 ------------------- ... => Trisoft.ISHRemote.OpenApiISH30.csproj} | 0 .../Trisoft.ISHRemote.csproj | 22 ++++++------ 8 files changed, 45 insertions(+), 108 deletions(-) delete mode 100644 Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj rename Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/{Trisoft.ISHRemote.OpenApiAM10.NET48.csproj => Trisoft.ISHRemote.OpenApiAM10.csproj} (100%) delete mode 100644 Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj rename Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/{Trisoft.ISHRemote.OpenApiISH30.NET48.csproj => Trisoft.ISHRemote.OpenApiISH30.csproj} (100%) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b95abaa2..62cadcaa 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -49,14 +49,17 @@ jobs: dotnet-version: 6.0.x - name: Explicit restore of multi-target/conditional ProjectReference and dependencies + shell: pwsh run: | - dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET48.csproj - dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj - dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET48.csproj - dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET60.csproj + # See Github #180 + # dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET48.csproj + # dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj + # dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET48.csproj + # dotnet restore Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET60.csproj dotnet restore Source/ISHRemote/ISHRemote.sln - name: Build Solution + shell: pwsh run: dotnet build --no-restore --no-incremental --configuration release Source/ISHRemote/ISHRemote.sln - name: Setup PowerShell PSScriptAnalyzer diff --git a/Doc/ReleaseNotes-ISHRemote-8.0.md b/Doc/ReleaseNotes-ISHRemote-8.0.md index 2554fe9a..520a4978 100644 --- a/Doc/ReleaseNotes-ISHRemote-8.0.md +++ b/Doc/ReleaseNotes-ISHRemote-8.0.md @@ -67,7 +67,19 @@ Below animation illustrates how you need to set up a Service Account resulting i ### User's Last Log On Timestamp Impact -The Tridion Docs User Profile as seen in the Settings > User profile overview (ISHCS/OrganizeSpace) shows the last log on date time (field `FISHLASTLOGINON`) which is only accurate for authentication over Tridion Docs Identity Provider (ISHID or before ISHSTS). When federating authentication the remote Secure Token Service (STS) is responsible. Do note that Access Management (ISHAM) User Profiles, even when logged in over Tridion Docs Identity Provider (ISHID) or any other federated Secure Token Service (STS) does get updated. +Since Tridion Docs 15/15.0.0, the Tridion Docs User Profile as seen in the Settings > User profile overview (ISHCS/OrganizeSpace) shows the last log on date time (field `FISHLASTLOGINON`) which is only accurate for authentication over Tridion Docs Identity Provider (ISHID or before ISHSTS). When federating authentication the remote Secure Token Service (STS) is responsible. Do note that Access Management (ISHAM) User Profiles, even when logged in over Tridion Docs Identity Provider (ISHID) or any other federated Secure Token Service (STS) does get updated. + +### Experimental OpenAPI REST API Proxies + +Since Tridion Docs 15/15.0.0 an OpenAPI REST API v3.0 was added on route for a full functional parity successor of the public SOAP v2.5 API on which ISHRemote originated. The outstanding challenge is that over time the internals of ISHRemote cmdlets will be rewired from SOAP to REST - in this ISHRemote release most cmdlets are SOAP as you can derive from protocols `WcfSoapWithWsTrust` and `WcfSoapWithOpenIdConnect`. + +If there is a new implementation, it can be selected over protocol `OpenApiWithOpenIdConnect`. If not, it will fall back to `WcfSoapWithOpenIdConnect`. The first step of side-by-side implementation is having access to authenticated proxies. Hence the introduction of _experimental future_ `InfoShareOpenApiWithOpenIdConnectConnection` which offers NSwag generated proxies to OpenAPI REST API of Tridion Docs 15/15.0.0 and matching Access Management 1.0 API. + + $ishSession = New-IshSession -WsBaseUrl "https://example.com/ISHWS/" -Protocol OpenApiWithOpenIdConnect + $json = $ishSession.OpenApiISH30Client.GetApplicationVersionAsync() + $json.Result + $json = $ishSession.OpenApiAM10Client.IdentityProvidersGetAsync() + $json.Result ## Implementation Details @@ -86,7 +98,6 @@ Code, especially around communication and authentication protocol, was heavily r * Renamed `InfoShareWcfSoapConnection.cs` and moved it to `Connection\InfoShareWcfSoapWithWsTrustConnection.cs` * Aligned implementation of new `Connection\InfoShareWcfSoapWithOpenIdConnectConnection.cs` with `Connection\InfoShareWcfSoapWithWsTrustConnection.cs` which should make it easier to extract these `\Connection\` classes if desired. But also removed anything refering to Explicit Issuer (unreachable code since ISHRemote v7.0) and anything regarding `/Internal/` or `/SDL/` realm detection as no longer needed in Tridion Docs 15 (only ISHSTS). -* Introduced _experimental future_ `InfoShareOpenApiWithOpenIdConnectConnection` which offers an NSwag generated proxy to private OpenAPI of Tridion Docs 15/15.0.0 Organize Space for experimentation. * Layout of `IshSession` was enriched with `AccessToken` through `ISHRemote.Format.ps1xml`. * Multi-platform code using pragma (e.g. `#if NET48`) for local redirect listener and system browser are * `IshConnectionConfiguration`: Web Service discovery happens over ‘https://ish.example.com/ISHWS/connectionconfiguration.xml’, especially the ServerVersion drives protocol detection and available API functions/behavior. Just like Publication Manager would do. @@ -118,6 +129,7 @@ Bcl.AsyncInterfaces.dll/System.Text.Encodings.Web.dll |PS5.1/NET4.8.1|System.Runtime.CompilerServices.Unsafe, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a|{System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a}| |PS5.1/NET4.8.1|System.Text.Encodings.Web, Version=5.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51|{System.Text.Encodings.Web, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51}| |PS5.1/NET4.8.1|_Only on Github Actions container, extended AppDomainModuleAssemblyInitializer to resolve CI/CD issues_|{System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51}| +|PS5.1/NET4.8.1|System.ComponentModel.Annotations, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a _for NET48/OpenApi clients_|System.ComponentModel.Annotations, Version=4.2.1.0, Culture=neutral| |PS7.3.6/NET6.0|IdentityModel, Version=6.1.0.0, Culture=neutral, PublicKeyToken=e7877f4675df049f|{IdentityModel, Version=6.1.0.0, Culture=neutral, PublicKeyToken=e7877f4675df049f}| ## Known Issues @@ -130,12 +142,12 @@ Bcl.AsyncInterfaces.dll/System.Text.Encodings.Web.dll * Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, but not mapped in the CMS to a User Profile over `FISHEXTERNALID` will `[-14] The access is denied because no profile match was found. 0`. Please make sure that the client (which you can find on the Access Management User Profile) is added in Organize Space on one CMS User Profile in the comma-seperated External Id field. * Authentication over Client Credentials Flow with valid `-ClientId`/`-ClientSecret` combination, and mapped in the CMS to a User Profile over `FISHEXTERNALID` which is disabled will error out with `[-6] Your account has been disabled. Please see your system administrator.`. Please make sure in Organize Space that the one CMS User Profile holding the client in the External Id field is an enabled profile. * Refresh Token is not used to refresh the Access Token in the background (seperate thread), it is only used to refresh when the next cmdlet is triggered before expiration. Authentication over either Client Credentials or System Browser was succesful but the Access Token expired. You do not need to create a `New-IShSession`, every cmdlet will attempt to get a token (either refresh or re-logon if required) based on the cmdlets (implicit) `-IShSession` parameter. -* Using `New-IshSession` parameter `-PSCredential` on 14SP4/14.0.4 or earlier works like before, as it means username/password authentication over protocol `WcfSoapWithWsTrust`. However, using `-PSCredential` on 15/15.0.0 means that you are using protocol `WcfSoapOverOpenIdConnect`, so expecting a client/secret. If you then provide username/password, you will get error `GetTokensOverClientCredentialsAsync Access Error[invalid_client]`. Note that you can force by adding `-Protocol WcfSoapWithWsTrust` to the `New-IshSession` cmdlet. +* Using `New-IshSession` parameter `-PSCredential` on 14SP4/14.0.4 or earlier works like before, as it means username/password authentication over protocol `WcfSoapWithWsTrust`. However, using `-PSCredential` on 15/15.0.0+ means that you are using protocol `WcfSoapOverOpenIdConnect`, so expecting a client/secret. If you then provide username/password, you will get error `GetTokensOverClientCredentialsAsync Access Error[invalid_client]`. Note that you can force by adding `-Protocol WcfSoapWithWsTrust` to the `New-IshSession` cmdlet. * On the Github Actions container-based build I received error `Could not load file or assembly 'System.ServiceModel.Primitives, Version=4.10.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.`. This PowerShell 7.2.x issue is seemingly resolved since 7.3.6 as mentioned [here](https://github.com/dotnet/wcf/issues/2862) and has to do with loading .NET Standard libaries in platform libraries (like Trisoft.ISHRemote.dll). Therefor extended the `continuous-integration.yml` to upgrade to PowerShell Preview using [pwshupdater](https://github.com/marketplace/actions/pwshupdater). ## Quality Assurance -Added more Invoke-Pester 5.3.0 Tests, see Github actions for the Windows PowerShell 5.1 and PowerShell 7+ hosts where +Added more Invoke-Pester 5.3.0 Tests, see Github actions for the Windows PowerShell 5.1 and PowerShell 7.2+ hosts where * the skipped are about SslPolicyErrors testing * the failed are about IMetadata bound fields (issue #58) diff --git a/Source/ISHRemote/ISHRemote.sln b/Source/ISHRemote/ISHRemote.sln index 0b5a2978..5970a257 100644 --- a/Source/ISHRemote/ISHRemote.sln +++ b/Source/ISHRemote/ISHRemote.sln @@ -5,13 +5,9 @@ VisualStudioVersion = 17.0.32802.463 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote", "Trisoft.ISHRemote\Trisoft.ISHRemote.csproj", "{A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10.NET60", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET60.csproj", "{98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.csproj", "{75C9855E-4ED9-4C28-8F1C-3B819C19CDAB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30.NET48", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET48.csproj", "{64DD5938-82FE-4486-B6EC-9D8271715ED5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiISH30.NET60", "Trisoft.ISHRemote.OpenApiISH30\Trisoft.ISHRemote.OpenApiISH30.NET60.csproj", "{247EF7C4-018C-4972-BC8C-A9B595A59EB9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10.NET48", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.NET48.csproj", "{896F5D8D-D254-4796-80BA-DA3F647DCC74}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trisoft.ISHRemote.OpenApiAM10", "Trisoft.ISHRemote.OpenApiAM10\Trisoft.ISHRemote.OpenApiAM10.csproj", "{3716E171-5FF6-4270-8790-EC92D10A5FA8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -23,22 +19,14 @@ Global {A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}.Debug|Any CPU.Build.0 = Debug|Any CPU {A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}.Release|Any CPU.ActiveCfg = Release|Any CPU {A86D41D8-600D-4FF2-BFF6-19C3D26CBB69}.Release|Any CPU.Build.0 = Release|Any CPU - {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {98DB32A3-E4B8-4FF4-B8EB-F6D25AD7FF1A}.Release|Any CPU.Build.0 = Release|Any CPU - {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {64DD5938-82FE-4486-B6EC-9D8271715ED5}.Release|Any CPU.Build.0 = Release|Any CPU - {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {247EF7C4-018C-4972-BC8C-A9B595A59EB9}.Release|Any CPU.Build.0 = Release|Any CPU - {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Debug|Any CPU.Build.0 = Debug|Any CPU - {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Release|Any CPU.ActiveCfg = Release|Any CPU - {896F5D8D-D254-4796-80BA-DA3F647DCC74}.Release|Any CPU.Build.0 = Release|Any CPU + {75C9855E-4ED9-4C28-8F1C-3B819C19CDAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75C9855E-4ED9-4C28-8F1C-3B819C19CDAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75C9855E-4ED9-4C28-8F1C-3B819C19CDAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75C9855E-4ED9-4C28-8F1C-3B819C19CDAB}.Release|Any CPU.Build.0 = Release|Any CPU + {3716E171-5FF6-4270-8790-EC92D10A5FA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3716E171-5FF6-4270-8790-EC92D10A5FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3716E171-5FF6-4270-8790-EC92D10A5FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3716E171-5FF6-4270-8790-EC92D10A5FA8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj deleted file mode 100644 index f797ee94..00000000 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET60.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - NSwagCSharp - Trisoft.ISHRemote.OpenApiAM10 - OpenApiAM10{controller}Client - /ExceptionClass:OpenApiAM10{controller}Exception /OperationGenerationMode:SingleClientFromOperationId /GenerateClientClasses:true /UseBaseUrl:true /GenerateBaseUrlProperty:true /UseHttpClientCreationMethod:true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET48.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj similarity index 100% rename from Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.NET48.csproj rename to Source/ISHRemote/Trisoft.ISHRemote.OpenApiAM10/Trisoft.ISHRemote.OpenApiAM10.csproj diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj deleted file mode 100644 index 7b4bb639..00000000 --- a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET60.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - NSwagCSharp - Trisoft.ISHRemote.OpenApiISH30 - OpenApiISH30{controller}Client - /ExceptionClass:OpenApiISH30{controller}Exception /OperationGenerationMode:SingleClientFromOperationId /GenerateClientClasses:true /UseBaseUrl:true /GenerateBaseUrlProperty:true /UseHttpClientCreationMethod:true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET48.csproj b/Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj similarity index 100% rename from Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.NET48.csproj rename to Source/ISHRemote/Trisoft.ISHRemote.OpenApiISH30/Trisoft.ISHRemote.OpenApiISH30.csproj diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj index 7e7d230c..33308746 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj @@ -1,4 +1,4 @@ - + net48;net6.0 @@ -43,16 +43,18 @@ - - - - - - - + + + From 6728fef1d4e5d96478b7d2722b0a68e7249c88ec Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Sun, 7 Apr 2024 18:48:02 +0200 Subject: [PATCH 08/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Third-party version bump to latest available across platforms --- Doc/ReleaseNotes-ISHRemote-8.0.md | 2 ++ .../Trisoft.ISHRemote.csproj | 26 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Doc/ReleaseNotes-ISHRemote-8.0.md b/Doc/ReleaseNotes-ISHRemote-8.0.md index 520a4978..cce5e993 100644 --- a/Doc/ReleaseNotes-ISHRemote-8.0.md +++ b/Doc/ReleaseNotes-ISHRemote-8.0.md @@ -87,6 +87,8 @@ If there is a new implementation, it can be selected over protocol `OpenApiWithO * Cmdlets `New-IshSession` and `Test-IshSession` received parameter `-Protocol`, `-ClientId` and `-ClientSecret`. #152 Thanks @ddemeyer * Cmdlets `New-IshSession` and `Test-IshSession` received parameter `-Timeout` and `-IgnoreSslPolicyErrors` on parameter group `Interactive` (renamed `ActiveDirectory` to cover System Browser flow next to NetworkCredentials flow). #152 Thanks @ddemeyer * Help of cmdlet `New-IshSession` was still suggesting obsolete parameter `-WsTrustIssuerUrl` in examples +* Experimental OpenAPI REST API Proxies #180 Thanks @ddemeyer +* Third-party version bump to latest available across platforms #180 Thanks @ddemeyer ## Breaking Changes - Cmdlets diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj index 33308746..4271e847 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj @@ -1,4 +1,4 @@ - + net48;net6.0 @@ -16,25 +16,25 @@ - + - - + + - - - - - - - + + + + + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From f61bf56b6000908f1f933c2b75123ee873419fd8 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Sun, 7 Apr 2024 19:32:12 +0200 Subject: [PATCH 09/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Cleaning up (platform) compilation warnings and info entries --- ...ShareOpenApiWithOpenIdConnectConnection.cs | 12 ++- .../AppDomainAssemblyResolveHelper.cs | 3 +- .../AppDomainModuleAssemblyInitializer.cs | 19 +--- .../HelperClasses/IshObfuscator.cs | 91 +++++++++---------- .../Objects/Public/IshSession.cs | 27 +++--- 5 files changed, 67 insertions(+), 85 deletions(-) diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs index f5ab4f89..7b3692ee 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareOpenApiWithOpenIdConnectConnection.cs @@ -95,11 +95,15 @@ public InfoShareOpenApiWithOpenIdConnectConnection(ILogger logger, HttpClient ht _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection reusing AccessToken[{ _connectionParameters.Tokens.AccessToken}] AccessTokenExpiration[{ _connectionParameters.Tokens.AccessTokenExpiration}]"); } _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection OpenApiISH30Client using infoShareWSBaseUri[{infoShareWSUrlForOpenApi}]"); - _openApiISH30Client = new OpenApiISH30Client(_httpClient); - _openApiISH30Client.BaseUrl = new Uri(infoShareWSUrlForOpenApi, "api").ToString(); + _openApiISH30Client = new OpenApiISH30Client(_httpClient) + { + BaseUrl = new Uri(infoShareWSUrlForOpenApi, "api").ToString() + }; _logger.WriteDebug($"InfoShareOpenApiWithOpenIdConnectConnection OpenApiAM10Client using IssuerUrl[{_connectionParameters.IssuerUrl}]"); - _openApiAM10Client = new OpenApiAM10Client(_httpClient); - _openApiAM10Client.BaseUrl = _connectionParameters.IssuerUrl.ToString(); + _openApiAM10Client = new OpenApiAM10Client(_httpClient) + { + BaseUrl = _connectionParameters.IssuerUrl.ToString() + }; } #endregion diff --git a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs index 919aa0f0..d0efb8c8 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainAssemblyResolveHelper.cs @@ -105,8 +105,7 @@ internal static void Redirect() internal static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { var name = new AssemblyName(args.Name).Name; - Assembly outAssembly = null; - _forcedLoadedAssemblies.TryGetValue(name, out outAssembly); + _forcedLoadedAssemblies.TryGetValue(name, out Assembly outAssembly); return outAssembly; } } diff --git a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs index 61baf8ca..dac1293f 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs @@ -63,19 +63,6 @@ public class AppDomainModuleAssemblyInitializer : IModuleAssemblyInitializer /// private static readonly ConcurrentDictionary _forcedLoadedAssemblies = new ConcurrentDictionary(); - private static string binaryFolderPath = Path.GetFullPath( - Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), - "..")); - - private static string binaryCommonAssembliesFolderPath = Path.Combine(binaryFolderPath, "Common"); - -#if NET48 - private static string binaryNetFrameworkAssembliesPath = Path.Combine(binaryFolderPath, "net48"); -#else - private static string binaryNetCoreAssembliesPath = Path.Join(binaryFolderPath, "net6.0"); -#endif - /// /// Early registration of my AssemblyResolve call. /// @@ -130,8 +117,7 @@ public void OnImport() private static Assembly ResolveAssembly_NetFramework(object sender, ResolveEventArgs args) { var name = new AssemblyName(args.Name).Name; - Assembly outAssembly = null; - _forcedLoadedAssemblies.TryGetValue(name, out outAssembly); + _forcedLoadedAssemblies.TryGetValue(name, out Assembly outAssembly); return outAssembly; /* @@ -171,8 +157,7 @@ private static Assembly ResolveAssembly_NetCore( AssemblyName assemblyName) { var name = assemblyName.Name; - Assembly outAssembly = null; - _forcedLoadedAssemblies.TryGetValue(name, out outAssembly); + _forcedLoadedAssemblies.TryGetValue(name, out Assembly outAssembly); return outAssembly; /* diff --git a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/IshObfuscator.cs b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/IshObfuscator.cs index 89a125e5..0ec31d9d 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/IshObfuscator.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/IshObfuscator.cs @@ -36,18 +36,16 @@ public static class IshObfuscator /// /// To replace words up to 20 characters with a fixed replacement word /// - private static string[] _shortWordSubstitutions = { "", "a", "be", "the", "easy", "would", "summer", "healthy", "zucchini", "breakfast", "chimpanzee", + private static readonly string[] _shortWordSubstitutions = { "", "a", "be", "the", "easy", "would", "summer", "healthy", "zucchini", "breakfast", "chimpanzee", "alternative", "professional", "extraordinary", "representative", "confidentiality", "extraterrestrial", "telecommunication", "bioinstrumentation", "psychophysiological", "internationalization" }; /// /// To replace words > 20 chars .. a part of this very long word can be taken /// - private static string _longWordSubstitution = string.Concat(Enumerable.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 100000)); + private static readonly string _longWordSubstitution = string.Concat(Enumerable.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 100000)); #endregion - #region Public xml obfuscation methods - /// /// Obfuscates the given xml file. /// When the obfuscation succeeds a file is created at the location given in outputFileLocation @@ -148,46 +146,6 @@ public static void ObfuscateXml(string inputFileLocation, string outputFileLocat } } - /// - /// Obfuscates the given image file. - /// For that, a new image is created with the same format, width and height and a yellow background (and the filename in text in the image if it is wide enough to put it there). - /// When the obfuscation succeeds a file is created at the location given in outputFileLocation - /// - /// The location of the input file - /// The location for the output file - public static void ObfuscateImage(string inputFileLocation, string outputFileLocation) - { -#if NET6_0_OR_GREATER - if (!OperatingSystem.IsWindows()) - { - throw new PlatformNotSupportedException($"Obfuscate Image is only supported on Windows platform (through NET6+ extension), obfuscating image inputFile[{inputFileLocation}] is skipped. [OS:{Environment.OSVersion}]"); - } -#endif - int width; - int height; - ImageFormat format; - PixelFormat pixelFormat; - var fileInfo = new FileInfo(inputFileLocation); - using (Stream stream = File.OpenRead(inputFileLocation)) - { - using (Image sourceImage = Image.FromStream(stream, false, false)) - { - width = sourceImage.Width; - height = sourceImage.Height; - format = sourceImage.RawFormat; - pixelFormat = sourceImage.PixelFormat; - } - } - // Creating the image with the pixelformat gives an error, so not passing it to CreateImageWithText, which means color depth will be different - var newImage = CreateImageWithText(fileInfo.Name, width, height); - newImage.Save(outputFileLocation, format); - } - - #endregion - - - #region Private methods - /// /// Writes an xml declaration with the correct encoding /// @@ -288,6 +246,45 @@ private static string ObfuscateWord(string word) } } +// Overall build should treat warnings as errors, hence: +// Disabling warning regarding 'This call site is reachable on Windows all versions.' +#pragma warning disable CA1416 + + /// + /// Obfuscates the given image file. + /// For that, a new image is created with the same format, width and height and a yellow background (and the filename in text in the image if it is wide enough to put it there). + /// When the obfuscation succeeds a file is created at the location given in outputFileLocation + /// + /// The location of the input file + /// The location for the output file + public static void ObfuscateImage(string inputFileLocation, string outputFileLocation) + { +#if NET6_0_OR_GREATER + if (!OperatingSystem.IsWindows()) + { + throw new PlatformNotSupportedException($"Obfuscate Image is only supported on Windows platform (through NET6+ extension), obfuscating image inputFile[{inputFileLocation}] is skipped. [OS:{Environment.OSVersion}]"); + } +#endif + int width; + int height; + ImageFormat format; + PixelFormat pixelFormat; + var fileInfo = new FileInfo(inputFileLocation); + using (Stream stream = File.OpenRead(inputFileLocation)) + { + using (Image sourceImage = Image.FromStream(stream, false, false)) + { + width = sourceImage.Width; + height = sourceImage.Height; + format = sourceImage.RawFormat; + pixelFormat = sourceImage.PixelFormat; + } + } + // Creating the image with the pixelformat gives an error, so not passing it to CreateImageWithText, which means color depth will be different + var newImage = CreateImageWithText(fileInfo.Name, width, height); + newImage.Save(outputFileLocation, format); + } + /// /// Creates an image with the given width and height and having the given text /// @@ -338,9 +335,11 @@ private static Image CreateImageWithText(String text, int width, int height) drawing.Dispose(); return img; - } - #endregion + +// Restoring warning regarding 'This call site is reachable on Windows all versions.' +#pragma warning restore CA1416 + } } diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs b/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs index eabdf97b..ea075825 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Objects/Public/IshSession.cs @@ -138,8 +138,10 @@ public IshSession(ILogger logger, string webServicesBaseUrl, string ishUserName, } ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; handler.SslProtocols = (System.Security.Authentication.SslProtocols)(SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13); - _httpClient = new HttpClient(handler); - _httpClient.Timeout = _timeout; + _httpClient = new HttpClient(handler) + { + Timeout = _timeout + }; // webServicesBaseUrl should have trailing slash, otherwise .NET throws unhandy "Reference to undeclared entity 'raquo'." error _webServicesBaseUri = (webServicesBaseUrl.EndsWith("/")) ? new Uri(webServicesBaseUrl) : new Uri(webServicesBaseUrl + "/"); _ishUserName = ishUserName == null ? Environment.UserName : ishUserName; @@ -302,8 +304,10 @@ internal IshTypeFieldSetup IshTypeFieldSetup { _logger.WriteDebug($"Loading TriDKXmlSetupFullExport_12_00_01..."); var triDKXmlSetupHelper = new TriDKXmlSetupHelper(_logger, Properties.Resouces.ISHTypeFieldSetup.TriDKXmlSetupFullExport_12_00_01); - _ishTypeFieldSetup = new IshTypeFieldSetup(_logger, triDKXmlSetupHelper.IshTypeFieldDefinition); - _ishTypeFieldSetup.StrictMetadataPreference = Enumerations.StrictMetadataPreference.Off; // Otherwise custom metadata fields are always removed as they are unknown for the default TriDKXmlSetup Resource + _ishTypeFieldSetup = new IshTypeFieldSetup(_logger, triDKXmlSetupHelper.IshTypeFieldDefinition) + { + StrictMetadataPreference = Enumerations.StrictMetadataPreference.Off // Otherwise custom metadata fields are always removed as they are unknown for the default TriDKXmlSetup Resource + }; } if (_serverVersion.MajorVersion == 13 || (_serverVersion.MajorVersion == 14 && _serverVersion.RevisionVersion < 4)) @@ -1174,18 +1178,9 @@ private void VerifyConnectionValidity() public void Dispose() { - if (_infoShareWcfSoapWithWsTrustConnection != null) - { - _infoShareWcfSoapWithWsTrustConnection.Dispose(); - } - if (_infoShareWcfSoapWithOpenIdConnectConnection != null) - { - _infoShareWcfSoapWithOpenIdConnectConnection.Dispose( ); - } - if (_infoShareOpenApiWithOpenIdConnectConnection != null) - { - _infoShareOpenApiWithOpenIdConnectConnection.Dispose( ); - } + _infoShareWcfSoapWithWsTrustConnection?.Dispose(); + _infoShareWcfSoapWithOpenIdConnectConnection?.Dispose( ); + _infoShareOpenApiWithOpenIdConnectConnection?.Dispose( ); } public void Close() { From da1c8914e3975d1ef9fa78240cdcf80e7f240ea8 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Wed, 10 Apr 2024 19:24:53 +0200 Subject: [PATCH 10/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Stabilizing tests across 15.0 and upcoming 15.1 also for bigger databases --- Doc/ReleaseNotes-ISHRemote-8.0.md | 2 +- .../GetIshBackgroundTask.Tests.ps1 | 38 +++++++++++++++---- .../EventMonitor/GetIshEvent.Tests.ps1 | 12 +++++- .../Field/GetIshMetadataField.Tests.ps1 | 26 ++++++++++++- .../Cmdlets/User/GetIshUser.Tests.ps1 | 7 +++- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/Doc/ReleaseNotes-ISHRemote-8.0.md b/Doc/ReleaseNotes-ISHRemote-8.0.md index cce5e993..6de155d0 100644 --- a/Doc/ReleaseNotes-ISHRemote-8.0.md +++ b/Doc/ReleaseNotes-ISHRemote-8.0.md @@ -162,7 +162,7 @@ Below is not an official performance compare, but a recurring thing noticed alon | ISHRemote 8.0.10425.0 | Windows PowerShell 5.1 on .NET 4.8.1 | WcfSoapWithOpenIdConnect | Tests completed in 472.44s AND Tests Passed: 1026, Failed: 0, Skipped: 3 NotRun: 0 | | ISHRemote 8.0.10425.0 | PowerShell 7.3.6 on .NET 7.0.0 | WcfSoapWithOpenIdConnect | Tests completed in 457.89s AND Tests Passed: 1026, Failed: 0, Skipped: 3 NotRun: 0 | | ISHRemote 8.0.10919.0 | PowerShell 7.4.0 on .NET 8.0.0 | WcfSoapWithOpenIdConnect | Tests completed in 449.72s AND Tests Passed: 1057, Failed: 0, Skipped: 3 NotRun: 0 | -| ISHRemote 8.0.10919.0 | Windows PowerShell 5.1 on .NET 4.8.1 | WcfSoapWithOpenIdConnect | Tests completed in 437.21s AND Tests Passed: 1057, Failed: 0, Skipped: 3 NotRun: 0 | +| ISHRemote 8.0.11207.0 | Windows PowerShell 5.1 on .NET 4.8.1 | WcfSoapWithOpenIdConnect | Tests completed in 464.79s AND Tests Passed: 1062, Failed: 0, Skipped: 3 NotRun: 0 | diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/BackgroundTask/GetIshBackgroundTask.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/BackgroundTask/GetIshBackgroundTask.Tests.ps1 index 97577265..10d30afa 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/BackgroundTask/GetIshBackgroundTask.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/BackgroundTask/GetIshBackgroundTask.Tests.ps1 @@ -8,6 +8,30 @@ BeforeAll { Describe "Get-IshBackgroundTask" -Tags "Create" { BeforeAll { + # Helper function that iteratively tries to find one BackgroundTask entry depending on availability + # Get one, start recent, expand from minutes to hour to day until you have one + function GetBackgroundTasks { + param ( + $ishSession, + $userFilter, + $requestedMetadata + ) + (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-5)) -UserFilter $userFilter -RequestedMetadata $requestedMetadata) + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter $userFilter -RequestedMetadata $requestedMetadata) + } + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddHours(-2)) -UserFilter $userFilter -RequestedMetadata $requestedMetadata) + } + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddHours(-24)) -UserFilter $userFilter -RequestedMetadata $requestedMetadata) + } + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddHours(-120)) -UserFilter $userFilter -RequestedMetadata $requestedMetadata) + } + return $ishBackgroundTasks + } + $requestedMetadata = Set-IshRequestedMetadataField -IshSession $ishSession -Name "FNAME" | Set-IshRequestedMetadataField -IshSession $ishSession -Name "FDOCUMENTTYPE" | Set-IshRequestedMetadataField -IshSession $ishSession -Name "READ-ACCESS" -ValueType Element | @@ -83,7 +107,7 @@ Describe "Get-IshBackgroundTask" -Tags "Create" { Set-IshRequestedMetadataField -IshSession $ishSession -Level Task -Name HISTORYID | Set-IshRequestedMetadataField -IshSession $ishSession -Level Task -Name EVENTTYPE | Set-IshRequestedMetadataField -IshSession $ishSession -Level Task -Name PROGRESSID - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -UserFilter All -RequestedMetadata $metadata)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter All -requestedMetadata $metadata)[0] } It "GetType().Name" { $ishBackgroundTask.GetType().Name | Should -BeExactly "IshBackgroundTask" @@ -139,14 +163,14 @@ Describe "Get-IshBackgroundTask" -Tags "Create" { $ishBackgroundTask.IshField.Count -ge 16 | Should -Be $true } It "Parameter RequestedMetadata only all of History level" { - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter All -RequestedMetadata $allHistMetadata)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter All -requestedMetadata $allHistMetadata)[0] $ishBackgroundTask.TaskRef -gt 0 | Should -Be $true #$ishBackgroundTask.HistoryRef -gt 0 | Should -Be $true $ishBackgroundTask.IshField.Count -ge 1 | Should -Be $true # At least 1 entries returned if BackgroundTask service is not running, otherwise more } It "Parameter RequestedMetadata PipelineObjectPreference=PSObjectNoteProperty" { $ishSession.PipelineObjectPreference | Should -Be "PSObjectNoteProperty" - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter All -RequestedMetadata $allMetadata)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter All -requestedMetadata $allMetadata)[0] $ishBackgroundTask.GetType().Name | Should -BeExactly "IshBackgroundTask" # and not PSObject [bool]($ishBackgroundTask.PSobject.Properties.name -match "status_task_element") | Should -Be $true [bool]($ishBackgroundTask.PSobject.Properties.name -match "userid_task_element") | Should -Be $true @@ -156,7 +180,7 @@ Describe "Get-IshBackgroundTask" -Tags "Create" { It "Parameter RequestedMetadata PipelineObjectPreference=Off" { $pipelineObjectPreference = $ishSession.PipelineObjectPreference $ishSession.PipelineObjectPreference = "Off" - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter All -RequestedMetadata $allMetadata)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter All -requestedMetadata $allMetadata)[0] $ishBackgroundTask.GetType().Name | Should -BeExactly "IshBackgroundTask" [bool]($ishBackgroundTask.PSobject.Properties.name -match "status_task_element") | Should -Be $false [bool]($ishBackgroundTask.PSobject.Properties.name -match "userid_task_element") | Should -Be $false @@ -164,7 +188,7 @@ Describe "Get-IshBackgroundTask" -Tags "Create" { $ishSession.PipelineObjectPreference = $pipelineObjectPreference } It "Parameter MetadataFilter Filter to exactly one" { - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter All -RequestedMetadata $allTaskMetadata)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter All -requestedMetadata $allTaskMetadata)[0] $filterMetadata = Set-IshMetadataFilterField -IshSession $ishSession -Level Task -Name USERID -ValueType Element -Value ($ishBackgroundTask | Get-IshMetadataField -IshSession $ishSession -Level Task -Name USERID -ValueType Element) | Set-IshMetadataFilterField -IshSession $ishSession -Level Task -Name TASKID -ValueType Element -Value ($ishBackgroundTask | Get-IshMetadataField -IshSession $ishSession -Level Task -Name TASKID) $ishBackgroundTaskArray = Get-IshBackgroundTask -IshSession $ishSession -MetadataFilter $filterMetadata @@ -175,7 +199,7 @@ Describe "Get-IshBackgroundTask" -Tags "Create" { { Get-IshBackgroundTask -IshSession $ishSession -IshBackgroundTask "INVALIDISHBACKGROUNDTASK" } | Should -Throw } It "Parameter IshBackgroundTask Single" { - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter Current)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter Current -requestedMetadata $metadata)[0] $taskId = $ishBackgroundTask | Get-IshMetadataField -IshSession $ishSession -Level Task -Name TASKID $ishBackgroundTaskArray = Get-IshBackgroundTask -IshSession $ishSession -IshBackgroundTask $ishBackgroundTask $ishBackgroundTaskArray.Count -ge 1 | Should -Be $true @@ -185,7 +209,7 @@ Describe "Get-IshBackgroundTask" -Tags "Create" { } #> It "Pipeline IshBackgroundTask Single" { - $ishBackgroundTask = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter Current)[0] + $ishBackgroundTask = (GetBackgroundTasks -ishSession $ishSession -userFilter Current -requestedMetadata $metadata)[0] $taskId = $ishBackgroundTask | Get-IshMetadataField -IshSession $ishSession -Level Task -Name TASKID $ishBackgroundTaskArray = $ishBackgroundTask | Get-IshBackgroundTask -IshSession $ishSession $ishBackgroundTaskArray.Count -ge 1 | Should -Be $true diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/EventMonitor/GetIshEvent.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/EventMonitor/GetIshEvent.Tests.ps1 index 1fc11992..4d89243b 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/EventMonitor/GetIshEvent.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/EventMonitor/GetIshEvent.Tests.ps1 @@ -128,10 +128,18 @@ Describe "Get-IshEvent" -Tags "Create" { $ishSession.DefaultRequestedMetadata = "Basic" $ishEvent = (Get-IshEvent -IShSession $ishSession)[0] $ishEvent.status.Length -gt 0 | Should -Be $true - $ishEvent.IshField.Count | Should -Be 9 + if((([Version]$ishSession.ServerVersion).Major -eq 15 -and ([Version]$ishSession.ServerVersion).Minor -ge 1) -or ([Version]$ishSession.ServerVersion).Major -ge 16) { + $ishEvent.IshField.Count | Should -Be 10 + } else { + $ishEvent.IshField.Count | Should -Be 9 + } $ishSession.DefaultRequestedMetadata = "All" $ishEvent = (Get-IshEvent -IShSession $ishSession)[0] - $ishEvent.IshField.Count | Should -Be 10 + if((([Version]$ishSession.ServerVersion).Major -eq 15 -and ([Version]$ishSession.ServerVersion).Minor -ge 1) -or ([Version]$ishSession.ServerVersion).Major -ge 16) { + $ishEvent.IshField.Count | Should -Be 12 + } else { + $ishEvent.IshField.Count | Should -Be 10 + } $ishSession.DefaultRequestedMetadata = $oldDefaultRequestedMetadata } It "Parameter ModifiedSince is now" { diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Field/GetIshMetadataField.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Field/GetIshMetadataField.Tests.ps1 index 9dd933ac..529f33ad 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Field/GetIshMetadataField.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Field/GetIshMetadataField.Tests.ps1 @@ -8,6 +8,29 @@ BeforeAll { Describe "Get-IshMetadataField" -Tags "Read" { BeforeAll { + # Helper function that iteratively tries to find one BackgroundTask entry depending on availability + # Get one, start recent, expand from minutes to hour to day until you have one + function GetBackgroundTasks { + param ( + $ishSession, + $userFilter + ) + (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-5)) -UserFilter $userFilter) + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddMinutes(-10)) -UserFilter $userFilter) + } + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddHours(-2)) -UserFilter $userFilter) + } + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddHours(-24)) -UserFilter $userFilter) + } + if ($ishBackgroundTasks.Count -eq 0) { + $ishBackgroundTasks = (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince ((Get-Date).AddHours(-120)) -UserFilter $userFilter) + } + return $ishBackgroundTasks + } + $requestedMetadata = Set-IshRequestedMetadataField -IshSession $ishSession -Name "FNAME" | Set-IshRequestedMetadataField -IshSession $ishSession -Name "FDOCUMENTTYPE" | Set-IshRequestedMetadataField -IshSession $ishSession -Name "READ-ACCESS" -ValueType Element @@ -85,8 +108,7 @@ Describe "Get-IshMetadataField" -Tags "Read" { { Get-IshMetadataField -IshSession $ishSession -Name "FNAME" -IshBackgroundTask "INVALIDBACKGROUNDTASK" } | Should -Throw } It "Pipeline IshBackgroundTask Multiple" { - #TODO test possibly is slow (or fails) when there are no EventMonitor entries - (Get-IshBackgroundTask -IshSession $ishSession -ModifiedSince (Get-Date).AddMonths(-3) | Get-IshMetadataField -IshSession $ishSession -Name "EVENTTYPE" -Level Task).Count -ge 0 | Should -Be $true + ((GetBackgroundTasks -ishSession $ishSession -userFilter All)[0] | Get-IshMetadataField -IshSession $ishSession -Name "EVENTTYPE" -Level Task).Count -ge 0 | Should -Be $true } } Context "Get-IshMetadataField IshFields.ToXml() for API Testing" { diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/User/GetIshUser.Tests.ps1 b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/User/GetIshUser.Tests.ps1 index 8dfca460..6f38886a 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/User/GetIshUser.Tests.ps1 +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/User/GetIshUser.Tests.ps1 @@ -55,7 +55,12 @@ Describe "Get-IshUser" -Tags "Create" { $ishObject = Get-IshUser -IshSession $ishSession $ishSession.DefaultRequestedMetadata = $oldDefaultRequestedMetadata $ishObject.GetType().Name | Should -BeExactly "IshUser" - $ishObject.IshField.Length | Should -Be 25 + if((([Version]$ishSession.ServerVersion).Major -eq 15 -and ([Version]$ishSession.ServerVersion).Minor -ge 1) -or ([Version]$ishSession.ServerVersion).Major -ge 16) { + $ishObject.IshField.Length | Should -Be 27 + } + else { + $ishObject.IshField.Length | Should -Be 25 + } } It "Parameter IshSession.DefaultRequestedMetadata=All on My-Metadata" { $oldDefaultRequestedMetadata = $ishSession.DefaultRequestedMetadata From 36700d74d71914b30677ca707a7eb30957942be9 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Thu, 11 Apr 2024 09:14:26 +0200 Subject: [PATCH 11/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Node.js 16 actions are deprecated. Please update the following actions to use Node.js 20: actions/checkout@v3, actions/setup-dotnet@v3, actions/upload-artifact@v3. For more information see: https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/ --- .github/workflows/continuous-integration.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 62cadcaa..760315d4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -41,10 +41,10 @@ jobs: echo "ISHGITHUB_REPOSITORY[$env:ISHGITHUB_REPOSITORY]" echo "ISHGITHUB_RUN_ID[$env:ISHGITHUB_RUN_ID]" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET 6.0.x - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 6.0.x @@ -81,7 +81,7 @@ jobs: } - name: Archive ISHRemote module - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ISHRemote-MainCI-Module path: Source/ISHRemote/Trisoft.ISHRemote/bin/Release/ISHRemote/ @@ -119,7 +119,7 @@ jobs: ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }} run: Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ISHRemote-MainCI-PesterPrerequisiteOnPowerShellCore path: Cmdlets.Pester.Tests.xml @@ -134,7 +134,7 @@ jobs: ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }} run: Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/ -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ISHRemote-MainCI-PesterOnPowerShellCore path: Cmdlets.Pester.Tests.xml @@ -173,7 +173,7 @@ jobs: ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }} run: Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ISHRemote-MainCI-PesterPrerequisiteOnWindowsPowerShell path: Cmdlets.Pester.Tests.xml @@ -188,7 +188,7 @@ jobs: ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }} run: Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/ -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ISHRemote-MainCI-PesterOnWindowsPowerShell path: Cmdlets.Pester.Tests.xml From 05e116a1ca6c63ebd72cb4fd93be6c5debb89d9f Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Fri, 12 Apr 2024 16:32:03 +0200 Subject: [PATCH 12/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... A picture to enrich https://community.rws.com/product-groups/tridion/tridion-docs/b/weblog/posts/automating-on-tridion-docs---installation-of-ishremote post --- ...SHRemote-8.0--InstallModuleFromPSGallery.png | Bin 0 -> 33372 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Doc/Images/ISHRemote-8.0--InstallModuleFromPSGallery.png diff --git a/Doc/Images/ISHRemote-8.0--InstallModuleFromPSGallery.png b/Doc/Images/ISHRemote-8.0--InstallModuleFromPSGallery.png new file mode 100644 index 0000000000000000000000000000000000000000..d16a141e3bab760397e34ef10ea6f6cc56567c90 GIT binary patch literal 33372 zcmdSBWmME**FH)}mq>R>isaDUDmlPVBHi8H4MWP%2m>SG0MbfJ4KdOspn!A;qI7dc zeV+6F*I8$+_k2AcW=;HR-+S+S?|tv;qRl4)ViU&FuY){KVA_eNnflZtRfD-2C!mMdUJ%1@9*iX88U+TXJp z)+BdYAB6f9jRcPkuMDm}bXMt4qIhC;$na!*c;tSa!8%!RfZ95_)v53d_4eoUd-@8Q zKR*c$u_5iZD;a0ffkzLVh2}R!RX_?_*+SHgPX{_eE{_mkDxPWE z&rtE_wd1naF4n>Sq#&$*D}#Js_9^TZ@nG<$!L3d@^>=KX+x>yy!%npnDd`Hu}2(ch66R&JuS>J!xY9tEXN z#DTceHeH%w+YE)4_?px(?fO!-&4cLP`u{vCaAf~^@K7LEroPA8xOt22hrV`Q(ojH5 zTs7>63P_7&!a>{wqZtBvQUEB|JSR|rB%I05?6rH zj9-`kS#WS;8jAuxdvg8FHVq5eP2-{dEueZ>puxldbD>+DH!#a5?v>FhV z9SG4f-JC!B=RDGO22+d*y-FNp#o5qd`=5b}N>A|~b?TG?)$pkmG85C>45k@N4&#QQ z(h$bBrV46;9Rkf3O&|z14he8SqK;BhSM9Nne3so(R;zQ)@gGlqk$SOIBoF!xde5sn zr>4a7L_N=hp({X@72=(ioO<_|Q?skYnLpZTE|7}=HC%-+VtV?sJ9KW~=+_mton3*qH;w3- zwsGG(o<}@TDOHC=*)Hb__?QxHj5zHRovRouxC0rJjRFuPAEwA(I=K-ei)B)c53nNR zpbRc4w40V?(3Ob){GX@UYcFD6%`(t(H7Ag;`>=koe6<;p&q)W&f4q~RJ1TS~pq3t! ze)eqK$R|zu?Xiwuz5w-;?$t_L*1-N!xV$lP?c+>c#P+M@S%nq81LOTNZhx1PiGO~f z_F3TIpNkLV{^8C6DPNN{ciz5;z!f=a3*K1S?A(KvG-UC*zay5fV&o3t?$nx za)yj6WDYyei|!1yGx?MLb1*JAWuVc1+^j2PAx1Bv=VH*@tVgZ)Fyv(P{`S)y*zEo| z{HCJ(4HrCFaC^l4X23nZX>5a0GA*!yKN;agH~semQz)P~2ik zR8Smpgx7@#CtDWWRBJ(AGv_39u$tAl@o(_R-HqS>T)FuXzOW(U@I2To{Q67y-%zJ1 zR?C0Cb{_U)!Tp2ehd~wDb z$U*j4;M3oOOoORG7oV9>^9ccL#ou|cej-0>UmTiUwKt1#HKjpNn+lOgZQ4UWDWyp% zD4YnzDVVW(*qA_G+1p1Qm8g?#c22MZ_hEd!Wf7!!)~p7G^XatOuHFnOfl5pX-9?Gt z#Jl(}u)z1qPTNSR+2fXHr4qy^qzKp%VkpV z*q!H=8w0tOSzko2+`D>3$lW)tY(li*Pz9bp2a!5rv>dikDvLWeRmPu`h`u|aa@fl^zm=kGMThrCd)WgeRx^6 zQSRZ_Uu_tEQIZipvLi+wc3rz=axTyDA?pN$i- z;2+1tx_%Qap&K=m$ld5j!Cy|`Sg;>B_KxCnt4|*FPsa3SKQYB#c%Zp_V>D})xQgKh z*g4KCZzmnvMz}T^#d5e)HD`>L!U@Qi{nkp4ttL}gJ9fl^D!~U$D^$qu-h`cRfPk}u zN}K`pyam#f_;`V%4o5|+6^Ny$*_SqC5@TwLL96@<`+D`tMtkHN;pf5m9>NMjG)VJ1 z#39h&Prj;GC{_bToxr{ve7m1#K3IkHwW=}Eb}V83fVE|x_w3c#pmJPe(bq2{W1N2^ zUpA6?T3cj6KrQIrssTKmV{x{vXB{v3Wzeuw<4D5HlP|e29v)}X{bD*cRnshr-JZ;$ z)@J5oyNsi?FEX`s)QTd%XBgXClg8mkul&xl)Y*_&TV{UH{cCrUI!;3GqGE@(0nq4$vii znv^i$>s{u$n(x!Y==3#JGPk)XP^Ng2^efq`9kv>B5aiQYbL~5Pt}3eLr69|^fy_=! zK&II`wr+M_u&y(8qSEKS@kG`(l`DNnNT?JC00bg1*!6-kPLkFMK zT@`|wMun(zZY5yw$>sDop2CrF+eN)C)HPx!qlJ?kF4!(yPZYPr5eElNRHBV_~e9(AISNx9Zp`k zqiA%b8rgc>Q%a}H7e{rgO451?1$KiB)IuMOZ>iQs?v!CG7h#hJ90qYePY(?$nXLAV zXn?|hWDVj|!hjlq8`wGtLN*x#kWH5Fc$?DB1B%auUu~N-gpLv9m04$UCIuqoF*qkD z%s_&q>4rHC5Gz621VPFT6&CUMNO#)GPK!0Ca_om+3}yJCFPfu(d)&1R;{4pTzFE6k z{3ifd(pgQZ4de{E!O*FTmHHiiHTfg$v2Ga^j@!w6Np-4EqRDBJ`Ao@+ietXxVYU0z zQYX^Oi5NU(;|ZJRiof1oW4vr4wc&L*aZE$L5I(v6IO`z)v&@uzUvJ3T7A3W#1~fph za9@vN4EC1hg))(0KnsZ|$&P&Te3?u_cb@7t-LCIO8Pzr;eqquK*x#va2JiVw?;1Hc zjG!%lP4S_w*^lNB-lPyRx6&Hz;H05x70?a{>xG^-KZ8e~tR1PKVphTl+gTj1I^KSY zPJ`Zltx13s6Z@XGFOn=`Z(;t8oglxuwmM0n{``RE5jjgX-(n!)5E ztgIP%RgsV?IsJ*nv^nwa{Zek<@!bdN6xh~ac}B%f9Z`vTqgcCCLfxUywLjrW;%MzL zp=_bhfmeKw^+nD-$juxj0rXf0juRiSeQj{`L}hbGxr6Qe{ow69|AXV#FSxR=D zXbz2$FHfjP)m69p>1QouH$ke$ZEGOP`4Vp^0_d|nz-vL`KUiAie4k6Ux_ zPeO1#scB?px|P}j9R=P^@osdOMCnFR6OYfzBQNwH03DTVh&%Mv@fEW2(etoVC?Z*D zY1yrERgfKT?R$718zeEjnI0%J|INF{4MT2|$wU-PtSy!y1Zm z&0ld6aS5;HezJ(2NzN6WjR<*u7%lSoa~wCJ6L_d)NQpd}&!A-ZtBNZ?8A$O`nTy5F zP$aO_8x_ylpdSeMH+%aC`w&HL@UIi26I5!|E|a9C{y|i)RzEm#p(8&-2V} zVa;q|_rU4cRF5Cc2o;)Ul^^RYiSWga1ily2Hg&Vrz+tcIj%F^b=AJ6zsFR#lFtu&K zJwjJbU3`m^Fh$T@SRu1PA{TUEMIj#y8Fnj@Cwi^wXb z%G*+c94}`yn631%o>s}zbatTvdxiz`YaLHoRsDkGMX}X5m&ba^B2<_dQD)p?I(D^i zQ)o?ZTlc`UmznZ1H^GTR%WtpOZ#(73uX~V0Q(~*4V35!q=W9 zYD~uaA3IwaB50MfFxWdSUiyE1|HslWw)mW0N)q2y1V}A!Xqvar7a;|reHE*fIjEQz zhy5sPDV7>)N?!%iPA(-S*Bf(#j)Pv!OT|Y^9xNP4)1KfjB@8w~945wLlSDFVMEM?= z(LxVPzDp|{>@W+7X}P*ROfK(0I+*2E8QW?;z?J%anB5iEC#YtVx~N(7Js_n4uUhLx za0LApN!b0z^&d_d5)oY|B61$nae7jT=;bywQa%1^_Zq-v+?4uSs^~C@JH43;CAwL= z90;#UxiSr#2G6H8>rx<~L1AcELyWC0*~D(Isp6Lhj$m&TorY*g>_^rp!)s}Zk zy7WGkWyx_3_;2D)@HFk#&+itt_Yc&(o*uFppf?%)W;Nd4d`fPsDL zmTu(&8#BYaBn{IOdZyW;u%bl3rXXX$X}3nk82Zsk5dDWmJ#N>0fQg+W7xRZCJisEF z^m?7r(^3PuLmc^iK#vs#Wm0De%fz#h2?A}dhZ>6;>B|hEAIeRyl2N|>t-L+GDILu6 zd5Ae}oP)o|jqCl?&+a7Lz+lVbQ)k6M^Ox0%nFm!VTd_s<;TPG5aj&2bN}pZSe8#{k zVoASrsRMJX-frJL0p?6d*9H1eG^idYef8&rZ-$7dS^N4-a+RC5C`f2^8_5^D3V{V) zuL`Mt3NT#U?ieZ|NDA~_hF^EQxFRrlk)bvYRnT_Lw69bk%g!|qf(4?}Iw)EJHZ6SE zObWj=zZatPc)rX?ux(8|O5a={-SeB)maHyZ>R@mSlgFcHG{rF<^lJNBzm^j6 zN}V!FLv!$k0>8luU%l?L5qgx>=sL{m1=twaQFqH=7=;PG2u|XkaXt57D}1$f(K~4% zx3qnMaTzlFTZ~u|z6Mn?j;#hZGzpIH4t>$`sQGdvYegY%nU*Gt%9^p+O*xet3O~x= zLvJJnGfDa*$J$Y*NzLFFxr__q&BNyu_Aj++j`&RX5N#PNK}mAa?4F=Eg?ALATP~UzBS?gv<@v(}R1~@dGu|D|Ej?Ya!Vq!m zJ-E&f>tY?Z&2Gr&)|tk|Il*+qhZ9XeNwx_{z`x-2$aI-~_(oQoo^Y}gK_#PFTI<#56CeZf^BZdJ5EyedI9kVl^5tgG`~uwPK2kC)aNt z59DVb^@?FhN4=hJjL#IScDf zhp`9UkQp~S$?t_Wn&Q1e9F)wpzINzi6?T**iqcO@{BXuiJ$fuxI2m|*V18eM>TKxr zGK&cmkes7R<~PSQ*tQOr<%#jfw2HRrq_*71D2L}%n)n1xI@!RUQ2T& z*js#dewFCRwnPHX<8bF`*dTp@2^B#8Wz$;mr!7U*tCqE6w?wE$Yd(pJ^*CD7eNUb`#j>pDI zA~Y!>qCPnL8`DDAvvFrWbWyKj&+L_BO=NxBplMt5-q|fJi0ZZn z>}-a_x8b^C%(V=YIqb~G6o?Ap08*$%Cr&Wt z%5;(LWs|}NsOJUV>c*N8!z;iZgNRpj6t?0xlvMY|kOgY+>cPP7GyYkt4$!{WvU(nj zrERjr41%DBOJx^?j4VIQ9nBa?E+^EttZ7f8lF^^FHrhPc30Ug%%0hMvN-8rfoURp) zE_8lY5|7F>QKp-HZM7?9(V=HR+pYd8B7_^#)bp&0tBP{Ba6Fwb#{JE)!%n3TSH@7F z%0>FAFJLGH;LQH`x1+`gu1vNy3;FiV9tYS{a!S3K+MU80W#hXCS*IDqJ2%M5Y-=Lc zLx^t+@aZAFzXMEh4$TS>_jFs|{4*U(QN0;-NG(P!qk_Q7OeeJlj)9!=9w<}4xhOvB zK|TN(r83=owor3H>n0eu$DNd#R@E`@`rMP6PN$TWGTCFiD+Xja7&1nV?4S z7+H4G&iL^p_)vs1}E9?bMtjCl3IsHQr6}R%RvlZpZIVepm3=>S|6b?SjKxmMZ&S7d=;a!)v-| z*WtSVdZ~r!IGVvl8u#x{zy;xCms@AyZjv}fn)i*A zas?rn%c*yug*@mA8)4f$=BYr({oVCS{zbqQ=b)zZ-}e8=*R9nlMlU>eX&b4xlvh;o zOP>_Oe|(>}lJ(|xOlu&%t$4~z;y6n}F!tXuZ1?q*$-E_ci;wDsI~+l9o2M2$b{)0J z+`8P)4&1}rhe^QGjDI`F-UfuzjHxwDp(m55mP^W?&K5nCJN&@e+}U?Jy#DrLJ#Dft zo+<3&XZzuYBy=0`4ZK?OPi{x9LtI|E87e_N+r{B`s2=w1`s1u0Jj|aI_2~veQBB6T zgF-3qX4SM4_DW;yQ-grGIZ*ZBohLXC)?bjYiM^$f_=GtJZbnt7rCw={SzUucw^z0I zZFdwoNGDN~7mle1?8MKYr2p@UEZXcm7dl@4XsQ4=O z81nqFLv#_LT96La^1D()pPoLK^adS%Um%UwV&7Yvx4r?VJhGEK-{gh8!kA2IIW(Ag z&Morv)veHYX>6Yw*F&y@@FY=i1y>M)A4~8dkppGkxY(yl&6Fx^c{mKt@PA8ZFjXWT z>NTf4BUiiWF>I_CqyxGoIFZoKRl-QmOwxL+1m!<`^iEMfP+#RvzJY~3Js$eSp4*9R zrIu|I*O@umTMlTKbL4{pag+joN9Xqvacwm}yOVOHA-YRB5d6Z80?5$=~jDsov zW|x_~x;6v_qfLas!LF5c5_X3%-KEb75dA8p_*G1c)s%C+J?KgB4F zy506A4=KrEygwH8QT zw3G+sqm@WjZv4)Ov%Sg(9QR8Yfpmei`3@RcZBNL67(89}e3G3n{CRKFjFMAx6IbP%n${~C)e?Bn1+6vmp5^#2tP{XdYBxc}lO z1Ex4y!GBGVENGbepP*+wlC|FSpPPAa{x@h2m`eQr^VBhG?clJZ%;K9qxf>-izW6k` zyNhXUIoGIv_AIwSwPp!#X3cLwWaZwXw95t3aiSZK%Zq4@nrb)`TVOVl{rpcfUxOMw~k4(`CV@62T>y1_h=3D3naF>MY7k zSM*YJ52Hm*ojd$$x_AJGjr?KR{j}Webc$ozBEKXFtUgzTn0)gv{QeqMob0K%{=@?7 zVb>@!vsbHH1G4iS`2x)@H1hmKqHDu%e>fxIzSsH?yNrp23K-h0=jv4s}$US_4tq+0@&{lbe z`SQXis_;+R#ah4Qn1?;j8`u-c7n(4nHO_wYK@CPnpn0;l6W`mb89r;9FT z15-aI@L+Q8IxLjH#Zy10;xn_`00&Pd9fJXsIR09fnz^zm85tz97ZHk#V7nvBtc+t= zJy(@xO9js@mMBPtu+d1{cqotKn?RDPAZ7Aa+22-GVmk6aPocb?X51~2ib^A1(JB0V zY-D_Mcm8SIvB1oRioTz;jEXIqYkPu_tDls~r^Q`yxZWCw;N_~ylb_ikw@OV<`JzYZ zv=T#yPoic$iin88ks*HeRwJyc;h)#!AW$&1ImASI)L+dbl+GzEIc^e;%Z@ZlhTOS>S56@NPzTo z)T)7FY9R|KK$ni0Zt}1jiYFx*5*^gDX0(%q2K{0rkLjC1A~=`bRN;qmi?ujxhv>>K zBp3CYGjS!9U$7HomW3g0n5RBBAT#QdO9g4GigKV-L;)2FdNlvaEk~3X-|4HXBQgOM z%NO!WIJQ}lcE%6JrVCGj_{1wtnTdUp7RG~~*oyqu@)YZE!kMY+>u=Vw20ONT{qJN3 zFv@Vq^+ihk7*(wuSvPp_*$yv*tsqo=FEu1sGkauad%0-A)-_`UrK2}WkN-Z>nlF~; zK+yl~XA*w$smHI&hLHclVXJuI**7-B^s^g_E7;ddO!ohrO5g!Db`p(+&IG4-)nND0uRJi=)Q@(?#~w6cuR9S|Jd$xs-)8L z&(YPpUn)LKF0Fayzf;@F1K(_W2WB`g0sU9+7{!#YJ*Eo-BCN+R5BoNo)ChiU3}ik3M)ToQdx*5YKph zbo%3qW)Wppp6PVr0gA+e%Kw(-Cc$y}d2UgWF3TDPoTrHF zsWeTBS$6kHyD=82cj)4O;N8sgSjiZ~orAWQeOtk!T=(}eqs;e^dx+!E+u8HbzKbLt zRUFYOA0ai~@4||v8-fS53p}(Jj|GyqVy*34YSP-ArELOeb{fZJoqN~q&a)b#+tYk! zQYF1n*`d8hrm%mOgLCYLRq{SXW>3XpQmn^xD63(pjDIs!nbCQ^-z)PlrCo|2#Tj9m zXPJlzUcWhC6*yoaO(O|u^7Sgcxw-UiV1_u683$9%Y0)cq#c~@5qkLgma!Pn^xmq@*7Zz-?khWJAfi$>{n)nm_b3HVW>6ryr*k)2d27#_tyKlsA<^ zl~U|%4PuBXQaT?mZm@qzIvtbUr*1iE(FxNxLm{NWfxJehkRV>_8n1d7{!G|pAax3Y z*9*$kxC4)xEQ+6$rC*{qfCFSU_)nYnhsE&X$?J{fU4=teIE`<`@pR>US(Pn+nx1f1v|iL(0g%%YFRa zmp(1u@-!5+8ev4&NOy#@VIkB&@>D8H%@Rj!erjP&kSd3M+zec{r63^UL;;m_ss5B; zOB|ADq zVo}Kj0RR)5RF;wtT_vg}(X3+A8PfFC7rn4AJp`p#<-varNSM$osWdMU_sB? z_@$D%$N{FE3y*aX!9`T8m4t6_y@|-j(nRGWt#GL!nmw$#>gYIi&(SQK>W?8m29f6LYoD5yl2bxW?dT30Xj$WdBDI;!JeH#zAC3K_HGH;M|sgi zk*L`X$F$PbSFG5i+&-VjDV0CTLsV&@nWfMGJs#ZYw*};iWwxFc1&!;QPTn&fBOefT|%iC=h{2$e2(uV zR^(fH?=hP*+?f2VH)YJ_`B7L|hN7j8UMjAQK81G%1BLtx)QYaks|o4x>Y%Jxa<-k zay8KQ0CrlMjv_xSreqv*hJWJ%B}DEZ9`e|?fhYqd6)Hy7x@5{IgY_t` zW4^tzf=_@6Wu;@FPO<^vuXZ5!T4y7bd}>4beu13u13fup)2 zs%I;ID0Ov9mmS+Ner9AZRF7#>u*kP&z^`TWOQtu(W7JzyhK?os`A+wgbDbl8dZY9Z zggL{TiY1uCu@WKl(Y&w$-&jv)Un!9%dcdhScX-5*(e95zS>41W1mVa57V!1^7Z7g; zZ1SDi#W!m&Byx*N>_(y*^6`{Yv{<%q7cR0xD^Y++$uAGUoGD>|Tgf1LB1GcpyAUdB zTs^|Yjeg77%;G9?MqNhDR=P4W0@cVUAjn=Cd@bsyxr%+o@eSbIOXR(o9ojN9L-I_9mZG5? zIv-E|5QIG|YB^#lJKRTah9BEAnTpX|XsD_5G!07%WfE}Jh!m)0a`6q<0nGuI-6T!g zB=?4d-+JQQi=7D>Un2J!LzI^z!ph9npJFU?0 z0Jm*BkxLAmS9InzKUmbH)|g~upW)?#RT?eJDz2#1zp>rf6b5iUA~IQ@6`}s4=?Ue0 zffk`yzn3BN72F;&&&aEequly6fLLGO7zT|RWHS07L`!Tu&2%0(4VF5PC+irnI0NQF z8fTA7jXO!&xf2e?{OO=^^=*@}YXdd~A<-cP+O5PhxXm;NN;HNv8@glz6VBwzi_pTn zLoS`q2!|PU8kbfc%O>P$Dx5~be4h)8r)`G@TBbi6g86KO_h%nMh5ohM3N_w_i6;$M zcj=}^(UH$lDm~4~Pq(X}S&chcvZFh-aw1nf@3ZRs;Gw@E^}$^cl<7Om$t|ro61d(e;sMx5 z3=UXL!c2rHv+)3xRJ02v=-J$Zudp}hBK0WhNM9~d_tp#uRSA_>zE&MY3r&Cs$~Mc~ zv{6TIVOhU?M&@d?Ze>>Ubu>$fE^pNE$ZA%08$dz0zvuX^h=}`?=OiLX0KF8{gdNoww7AakhKT>##u4haSjQJ?q z(gNFaAh#~zrnGGerU0DK5?Ezz_N}GT0;NzX(>e0*dIwRI6FaOcx4E{326G@UAR2Wr zi!%EQ*Z4*=j!?TC(RwjTc|`~{wc$Y~Vn%s`jA9Il`>}OaLRP3fiRs$yX4HSaBZ89D z&jZQvNvPhz6rKhpSK`;z^7yvSJy9XEujZ(Y6uzLeR+xv7@L+--{f@_IPDb#Sb9R@j zMZm4VQ|$1hQ=WOMKzvx4sY=*X{jl?oSnON8S|qE^k)>cql|{PBTN#$Dax>eW38%h< z3pNrkH`N%GMWmTi?Q`Ew9VhY;(b>KR&I9yb_$3!BZ|e!UkD9!;`6!dVR8&@`$TIh5 z41xFf=%O!+@%kMIUq(^$*#Z}nU3m=Ex7}L=hOe;^M z1TQMXD%6uu(1=9S_eHUGRAD==9B?%cZ=eiO!-ov1o=|(pY#?!%lw$beKstF~O8IE2 z?r&+yUrwJf{JqG@k~{@bZ?KK^2a%a20t_r>+rKsP>6+D?K+CVo%ZRV~NPeQh(xK}* zD&65r2(C{8Q6ATus0>^2qng#s#USDwApq2Awok$rfCfY-C1mOR*0S~lD^#}NeUzLA z4wDU8&RhO$A+#q5Hn{*A@!^tYKDr-NG};j}0ewPbut4 z;m}5fnxllK$$l_*-_T_gJy-Q+6{!Ew2!DLMCHsNkmv~W6vSdN`=$}oVCBI_UEr+8U zhqnhWgB_gSU%!|n)xnt%9^}OF=2`yfy5Kmz9u+#^cD=ta%=Cj;$ z8L_byrADN$Irj?`Lv!F_!$J%3^3zkPgVwNln|%SDxY4+R@O_8mz-N5t0_Xb+!apBh zNe%`wM*q3&&5thK+@7Ou~ zckCy277DA4(6h*&HtZqs5P3I8t@n%4ofv@?%=D$9xXzs+8}vz!*j@BqY}I)!^1MSr zY3#|tq&b1*vB+guw=7Qc)v;rZ_P}y5d?)f|QNH!9JjgW4m%W%h(Z|NL6L9h5+ zWyLy91E0F>GgO5X2@u$+MYif-i+-(7OSw)M;GC8#(0AFmv+wmRMM@7&mlOfe$>!`D*_4?O(l(-J8sKhR&NcO>#X>wxvhh&tC)=<}| zpElJ#UU5+SKSBG4i1wlGz1-;vahP(a1}wxQw>8@VE`&8EnVe@&%QRR@E3$t$xCQoG zzASe8E9Z-VTrG_xybqGz_KfR?XLXtLsw5tIb73jlG_fh?b11>y9Jgh+ROs=2lP~6X zUowCH{f7~iVC^3T{*j3JLgMbFcp%^eHo84te5^AQYmq$vabfH}YU#kzvxgGIrs`&a z6H2C~V4qG(%5AF9mTa%tTLP>7q0$!wOwYKJ%=W`>PcR@q^mJx% zoAY2M$$Dn=1QSoNaN@w=I{BZrY6^p|h&vIaV zba@czz&CK@k&!VWoA`pOX!nz)i?<}_78@*9jLz#FJFms)Hg8rIRF9*809CqzGfm%u zf27zwjM#BVprP>ho(Q|op5cNWM};MxXyjxQ$JpVOx>Ru!J{;C5j;^1Q!t1CnXC3yV zy$Mw}c%nrQ7VTc1)LDoeN%6(!Pul(9wGlKi`vz%FXvycdOs;|rU}0O4Oxx}GL|~7Q zbG|D@FWqW)Z>%}vaU*6^6~m#H;HMOmKd>a_>UsEg&x*H`Se;TJfd548Kqt60Wf1UKZkA)O;LZ;UaC4m-i}_P zYBE~`2;5EECIe$MphBv%RQ(mgKpxMDt!&;Q-QQIWlw$E?kjqGlP@-fJwXOw&nKD=^ zS;7q-&-n(c#@3&V zz&MQmEh^y7%akur!oroDtsAZTb>nMc$T(Gono_XSw%}W1(Jc2YbXs229<;VYOQ&Ej zXM=ZL8v`#kPAA&n=~O~satE@PnG8@q9epg?dOR|inF|KKAfJQ1Scx)hq&C})_tiE} z<3U}mP#qPDPpXIEYD3vVZFM+@O(ipfkW(Xg3!Hi6lPFZ$pFx+> zfX(E&c5ZQo8pfMZz<}ljKoWk*g4ZsvfG&X{I84k-_l}GVX`)tAuTRjD^sMB}Lzy(t z*vu5E6fuy~mtfWCODXwVu_Bq%XWK1qdy-L8Zz-os$0CjGnlt$#q?k&ox_YrN2WjN1JST?ue2W;BD2XJlJt(4kgwZl4XC z;$^n3n=@rVESHs8&-1eohQI-pjaRHoq}&2ru?d!4p!ZRpS6-Kcb0VCUG6#8wC4Z(w z6y?jIDAXPW0mIawQGQ|9S`Nt2&{_xOAO^no7NzQh?m(;8+l<&$ftDT1t~y~Sj_au% z!Qoj7ssbb5=qgFRbrhp>U@bewQ+fZPpRzAHgm5zEE15!Zm89Uu5dj^y?2=o0<39-Z zHbNdUtHHdMi_hi`c6jgy$xzkf8^{NvJEzG0+=nb+^H3%CfFs3&Gr}vOULE+57eIn91Tc2h~S(}ulR zta%8%XqgML7>1P8^=UR#$p$7Xxvi+zd+P_JR-hhT^JoIJZMcv>R9J4fYVx;kEsY09 z9b6;EDJ6tfI5#sd%jjgNTO)-&rrF7(la}EdHZ=cbfw1zV|w%md#8O=wCA8|{YQBGTtWHwk-PpwKH zt(|cXx+XMyOQ?88$({Crh->kPr|(hx@6_Htmyqj@4c7~30ePy*>{^GU7a;0#09!(I zF&v42<>4tBJ^*7ir!??G&fod*enw3{~a~m=qpSO0Gct zWT%&0DP^iL+1p5kRb~?;bu?RKP{FtESB-F){3^9AzF=r;+ve`XCk2`!^~SnAKYm~nWny%%N7)xyY?sF!#QEvr?cw^Dg4 zWcu?#GmDM-kYi0#5|;NG&FEt?{L{7al{5}dP`+Bv@=(E5e=ew&mQd_Ng^Wx2uGQrp zC;H`9nP`Np@+%b|ARwX(&eP4#N0CwrwNwB#_x!E8@_LDoKKe8^%FeF>DjOg(T1BGK zZM`%ZJWK6;U2#S31woC1p2=zyIy#_LOGYZ25MMK=a-+xrP_30&gMjfK4CvxQU3276 zEvfToIVw}CFN|MX8uJXrXU!l%lno|N=&(oU8DwWkZq^@po z?sA^YsX+#&W^UyYYKw&|^lddN;ws6aG8aM@sg&TLvvn(bcEfe#ONga39pO(4n8hYP zo|Sqlk-gazpT+i#Ci}yzrkF?#jucww99^ZZ@u<9?zFEy4R6+k8&4$@3_Ri}BILM>m zvwW!KX*r=!;;O~xGei}2tkI;5xk+=63JHPaws0)nA5NhFSN<=X4VKt)lYA zTW^`5}AFbeEvgn8I zOa#b`-dRNFp=+B`c#l;8g2pCj_NmWReF>A0b;M`kC%*+Zkz~N7Qa8&bB#!xj%9afkRYAS`Nl`! zI>-hleXA!RCga>rcpw)PQYFpw{_%mOaDAC<`{kn=L8+FgrftND zZC(4#%;^3)sp@jUD7Cf0@0z3PmhVmRZ~8?s$|m=D!f73T_M)Iw)K2|%o(VDZ!W+5A zX#z*fE(6q!N$y$O!&ntEkELBWE^dy;S2V(n>Ojc49Jl}HZvm(q6aI(V()jx92DGMqf1+LBH}VIkp9{jM2bmI&o+hdZZGyML z@6ztcYvnH24@C}gx;FklYPo6lA%l)C9jIP|I6fLO9pl*I#Jblh7clo@kg`RO0 zqKz123fFk>0Nrshv2-u><`A`o_U^3qUPodC=s3SM!# zz>Z2@Uq|jMvY!!QK)pZbrG+_O%y!i?;Th3=AvJ-xC#g+(=zj>3^ z#SLV3(+>VXE&UW6oH$Y9-Wzdn{m|wYigJ_nj9;1e*|$Lo_YXh|5z6LYn>prVm;p1T z-SD194ENuSc>WRVThhKPf&sTzBD?9tFvzxC;uo4w>&59e>$5rM9Q?mGhRKqn8yd=G zu#8@w7Md&qEK^W;^i-~VhVrE6Ij2RH9*5P;*geZFCB$S#a3ulb8M=Lg<}O>SN%fl_ zSgee_T?;hUV|lRYqCd=%zCjd*Kh3csJ=uQf?h-!Y4!E!&cl$xE5{=LslF&BZey#G+ zUr!lR=I=i@$WrQ2*>eBc-pgLZbMTI+C>znKyfcA9j?Y4Ig)WYZ?@ia9pADmihv@cN z;EB`s1bRDSehH{55_bZk6!PQ`%^QAd54*eWsvx%uyW~Tyaan0^?456o-SoLS8t=baJKfoaTFnU%g?RfK>_JK(4c; z_F|uP?s2?q^~8F}ogf03cDeX}Rrl6WQNP{4H%JN$A>E~PN)6p1odOC-w@6AT(p@5h z)BpoR&VaOp1JYeX2qGX|0@4bioX_~Z@AJFQ6YDwadDe5*I{xQk9J#Lj+1F=Zd++ya zvpE^xf+*#IL0A{(rNKULMzLj+yuz+_%S}aP6a+~93rxyFJwoyx{vMZwa!I_s9&(_b zLGz^Msn24JS5MXc=X&B5?eSeV1G^)|KXEP?*?WBKOgug->c(U0D_*zkSDk2~nk5e4 z>MTg$%J1H6tK2kjn1cN3*&<3NU{hqi3|^~H=OuYAwx_l)yvzi2$$lWR(=^D5F{DmD zl(ZJ^^?u+{By_j*EBZY;r;CwsU0@W^*?F{d z_SmMn8278IZc);kMf?Mr+5TR%@WpTi;@2Vxaj*A?a6tmc2__X-&`59w#klr(gZC@) z2sq2B$o;`}dU*zhqnDf5qZt-vG9>KA_k6c%^U}Y7@u}$4-)XZ#w;{)g&Lqm3`h0tM zh%tv0smWgP%LAKkAm@Vno^N@K)>8K79nX$ z5q!wqRvIoq3$(|s<@4Q>PBKGHGaH!&MohE2jB$+`+H?*5JpA~jCq1`c<#sHfbmcc1 zK|k!N%$j`Ac3WzPL{GS_g4Xl0i9C0#I)daRl@Y~1WHdXddabDId}YQoOVrx+L5)jC zkY?>VCDf2!hGL5$CFYJS9mL6X)}b48{&XP$NX!-63Atl2hVk?2qVIi?1Iz#RQB;Yp zSdjbHXG4FrC8VN#C!4FrNNt(!^x=HY1y-6&L8SOf*JslNU~lcwYz$|Bto_GMT&j%p ztfJ#mSMe82OW=;Qkm1(rBE$-`JlWj8w}UAz_tg9T|y>zh%xB*Lqw z^360Um8aUmN1ObSy19Cy4|?GvMX^d!r1~`)Tl1jDf^_`Dek002{8QZvoOeMDG2F~n zZ%EVxU(59paCOy5#ie(-sXY1jruHT7ye$@?0k$*|&v;8!ng7oEEgI7RO)!F0iuW@3}{Qa7k4al2fLFfo57msyXICw%bFiwuhspr-wh#!`6h&4_?Gikv?-n&Es*? zO0Du)Z={Gmi}~%A#`5}dHB0(Jlq?A!J&HcfGmU6q;4tvg$4~k^M=AmiWL)1_Czwwj zVTaEyd5@d)BSqdQ=V-D@e6vWwm;CSm@2?|g4H{$o_{Ye{pY4=o@_tcNruS$D7O*L} z-s`nQ{C(%ZFYE*ekV70({E}FBW(ihiV(zUIE0#|s<+|dyMs0;iG#P~^%Gx3jZQnWk zqNbxea*b$`@^GF4r#T0-NJKMf>b$tU_tJ_^sCf|-*KM?C%=$P$h57sd=A!`OcS%xXh}OE|%CKZnYh~<8%9`UuK5i7*mZ8`g1LP zd0YELX6wZdMHXhS489DLgQJJ6N(XtihW|WOg%>kqC(LP=eL}9_O~cHD!cb#2zZI<# zR;h6VMp+C0Jod@J#x_Q8lFeX!%O~Mo`WerZ=m)rf7}J>{sBP??k9|q&A^Lf@u{Y)~}22|x6erS>fcq1p{zH{$p zVILZB$auaa@7V4guj%ut5F?nfKP}m73=kwdb#+{4qRzC`#XD z?$3N;Y%0)iutUOpPF+R9`F!a^qf1=e#vPJq=FwXk#d>oEZ!M$WDSKyCDFLB8zVosD z)w7~2e`142eL~#_KcPn!&o(!h6~p;Bo+StKLtiZPWc4eEDw7e<;d`IFEKi@O;_cGdDd~2WzRqS;3s;_u*R3uCXjBy{Rw#44Jy`^%;&+1?CBb zG-eIHF#_-jSWkS{b*-S5m3&vv{WT5FGkutiHHWAt)B6E=->|pA$5#z~^qXQ!UAeeq zPJa&44|TkdEGm>-;$1(lqdVf!u_|}ztTUC8(PH;?il;Ul3~xyB@C$@if$C|Bzph#^e-pW!?#h%b;=*muby=;Gx-6WST z@Di8)RN>=47pz6VrZ4CU9x!CdFOC#M+fwCp3tU07sm*>_=vw=}J%P`$qPglJwt)-N z^e=xZx`iwB#JbH=0bkN`Du|CyS)^ixJ z?~5b~AmS(RHl@3okN9Y!CoE>5#3)u_H%ITmQw$BG<~ ze0un*3cD&mfvtG|l|Xe|C?(hU%i;Nr#c7%HiFk1G-b^ukVr?Q+>~V2K`Vp(VYI9<} zJ8M#<&bYBc(9N9h+dKREFShVmf09`Q`j2Vkxl;gJDUd?aU(EDDD$--gb3TXQtsFa- zY#U$NBS^O`phJ_q9g}Nhw#YP-yl>RCoe#tdw%oPJ=I7lE90c~8+^Do`D5ppA8%3o- zI1fE+a_^Iz9MgqMA-CgK9?d|6y}%TSfQJy`PLc@?M0x;8^l-XBqyUW&P2&1`UA=AQ zAz?!Ih$%Z#z-6155Dj@QcrC3U0CO?QMQKoAHVw#IuF?;klC718;LaUAu!FzMOhmxQ z&>O!JhHpphq6gB_;M!koBMDYZ5h~NU&C&OD*_vA&mYtHN4FCd=d;<{W1xL*D3B_$) z^>XH%sKc2wYT#2W?f>H)%ttw?dy+Q&g6V!XW2*KGTzh1Y1zm2GFXi)N&npA)vym{$ zUX|CWl9{4gZ59tO3I{9ktTz`{JisFf{6&rgBW2ji1^RVsFnzh%I$i&6(cc>W51qug z%t7$U#t;&816q-5{ZBa^onHbpVBvK6j10Z(`D@O0yI);4%ohbQ?`hXq|J>kG{`<)* zy5umyHEMC7H<(4Nr&04`8NIjtpyJoi^S#VJx3du7$Bl_^xD5(W1%I&p{H=cHh~l8% zr0R8mmP9$CPy>pg-=MTRPESb>DpMkDja|i?iuuwqL&Li-)8p)^Y6<7NW6TW89bIr*t=a5b$=jLqKxRP)#x@G8gK6~v0L4469tq4B3WcF-Ar`_}Y zxOdvy>~5rG+-+h&xJhj4WeHY1F*M_$?a50~-DW2c#(QUfE8V`V>_lEe=R7Y59YZq6DS$1=vb-3CQVdh|5g0oBN=oz;2QOh5~fJ!0>%fO8D} zH1wyU#?hM;M$DXt!cB7|H9nmPn`xGH7bsAaNd%||u95{tzW*0+AwVat1D>Cg)_A%h zGN};*Zj=xEW7Te`A?&H|95RDPGjIoJ12KtJmkPh!;&nL|xj38l_w@Bos&~(6NkvS# zggplfw}mI!+w3%)?v2ZC_91FtZt3vSz(sQ`ZK#^aW)wP87l=ce5|5Y3r&E9Oy{VPi z%O`r+M5g}i0=jBry^EK&Xb>-LPtq%R98>rt&k7$B-k}d? zjqqR#sl-k%$HrU!V55D-6#3#k%(4I~y(2R|UkAw+M z{5NC)tzqe-E}nCUxg|6U0-&aVmd=n|BlTZErvdPMFg%`Sk!KDT(tde$R>`3C`SLDE*tasaYd(FgnngdRlS>+`vRa&V=VXUIh5E_@q6Eqn5q(l)D=ti* zL+NK4bqO)2AN!VQZfP6$*3|BOC9sri9fxm);;>N@X_n&1xQg6kHBSWg>`U8kIa$$d<_f#M3Xpx_O zewhz?S~5kMAyv1?B7-&lx-JpFTiQy*59jM2X3|NMWv`gt2&3^i^%Pis)7>u^2w4_u zREyBy#`+iagLj;#Rl6{>Qaj>}jkoq-O3{BXKQhZSQnmjwKO$lS5#s*B(A0i9r)H!*|pi4@H$hO)*Qi6bmhA+Y5JJf9yX)e5ISRtS~3Q1)ELQH%oHWg(r zr30SIY(~stG)!!q#A`-ptLnLKMd$wv)WOKPte7;fy$mMhZ_fNT>^^T`0~h3mAsz3B zJS^{)BB&|+QRMRz+;^<@FvyHt&nHiTvbx-_p5~YJySLOCA34hw`~5w7vbqO*tnSIX z@Fp-((ucClV<E98Di-ztJGATVNQ}pfR%y|USD^R9o=#8>sZN*X; zWOG`s?54uI>{lxre$1Wu(u#1V<#zJ~&y{7RT$bgqdzq3RC(|TZ32%j@Qj`KzSWciD z+0UnXQHy+I&Y(XgawnoYXorFq`C5WVc$|&TZ16&w+m3vLd*`kU0}@Am3~1$q5KGP6 zxo5@`;ao+fpru;97$3oQVHc#OZ1xNK!+1(SlkQ~a7N~fOBdRsYo(^PMMr=maEh;l? zc8=J_i_h0cE)$@6rk3{d?*Ywk3*>?P(g)<4LTpar$KqmdvFyoN1&bhsHNd)!ZM)c2 z<-Qp%J3}ckTn5%>;j}-K@!O%_4QbQUh_Q2D8X7Xv<1&o+U51NI1v;dZO-errvWTV~ z8|>*r(2G0p14S1obC&$9QBkUHBTSx7 zw?Xs;A?G(yS@$(n{o$(ZJV~OafdW#O+3oBid7L@)dh`|Dj{>QMLn;shwHP7VD76Si z!Ot=zm;HfN@Cgy^!S&Fwu?;BoubOnc@UL9Iz3q1jgm~>g<^2w$mwtBtj!F8N{;fy< zzm;aXgGU z)GkR6jZPe|0!yJe#p`<$$@KAW{XI{f?xa%CZ!oVzhbGhhG{dUK&^Msu{|}MNB_J?b zd3^1+!EEv>8i~sL8gh|`0Sx}eBrD?rmTxc_fT$l~)w3z=4_02@Jo|J0(eng<@E%7^ zygBmANS)W@n+p^sn_fEta2nyADx=7}`#xL?h+8StW~1tVp_W-dFvtP~o&^w?72es* z#=qH^F}eF*C|U-Ojc4kxLTEVNtNj?);(QTex| zWn9txS@{u`XG5ZGle8SzwxtsxiMC=@hx)NSK=_pc5I@If6{IC=>*r@3=xj5v=!{3uk_K8Q$ zK1x9rhP$g-qv#H(Eae#9m&ub)xaIGsU1{TQW3^4z4e1+5=y&XjOsAsA{hBZ!#!V@ z8{xf@`pm8`>#KiN;l~-L%syr5%^Ls%=?L}}ai{no9A(6av-$>7G$b6r9`icB>-u+H zIJ>WVI=z{o1`f{-r+`g~xUa6+d_uhv#3#V=`r=U|g>|onSEoI!H|&~hVkRkk!)D2` zflkBQ*xBr%pbf8IhQeZ0Hvlw{PLy{`E9j6T4uT-q#TT0jGyeKH7wkvk1?r0|ZYc2W)p ze@c#d<1Ay~F?1mtUBN^?om0nnGwD4&uqvldr|72Fj;TnceUa2O4@7VI)(k+{3D4coixoFiN}aH$)%u%D$&M*pWsjH1SYWS9HV+m1%UK1_> z05%#X;4oOZ2P!ustug7rVRCn4Qrw>i#mSG|7G`Lsaay1(4|7uB`!Su^>WW^$;P$W7K7^6Dqg-=2Jwqm1lOA#>n%mzThj-nCLah{pf=C}3Kds|Yhly)P zOC>>qACTo&9QE5*a>iU*)Zw1L2VOPszihx$)6}?%6?TNP97UeH(kr>|Q7(S3$?>uKjfD;9;9RstQC@=#@DD7g?vn)i=;UZlB=-}|z;07c6e_w5 z8QzhZin?~g84l^I90Ns~Nogc+-K%f{xAN(27qxj<5#^j(IZcWD8xUw)K;h&&{#uNN z*-v$}2E4S@p*)^{!Y?(g#FHmSvF)b(G`@t0>54+yAOnX@TX?444@c|HE1-hPyygV` z@lc9Al+p`0*kUe|e72{%9*FZfXf~lBBcfSainug0$+xvt|BD@XmZVfPDmJYAjq6JS z{OJDQ7O-oHX4_=Z$3Wr(mkcGf{;_7*GFtL{{q@TPNjkTaK-pnO>8h zzE{=R*NFyq&ocSM?=-7s(Q&Mh0C+yB@&hhi=}}A}YG7`bLq0s6v2oRPd`9G+$C7Bf z>{k~WWNj@SXt{EDI;UJdWR;9YqwqyVmOF1Wj`mv)lVU4@VIITJY&>lKC5w7*h2U>< zWDa;S-V6t?1-^(pyeu#0Bpcx(d#%h;2=Im*RF1t5NUP#u>oJ|L6w+6W%Wnhu2ghIT z@Zh5D&#WDr8G5ul3zPT1P~`uEQ>M~j+2Y%k)_JgzJ-Bn)xrH_xWEkU5wa>43dpgUM zGK*G+X9LL5O)$0tcIf*dfo`o>RhBE%)5y^AB&If4w)u8-+XS-YBdnAv_>6?%Y(_FT zmOwoLJN3*I|=Vc}>fya1@%oZOvIwFtL)70n9L*W%G z9cp0+!ded3MN8kcEKL~v7@39eCpV{`jMI{ih1KAjQ46#cnd0W@M%?x#-&U6_{z$|I zp&kT{I&ae=6I};`?_)zCQf2buJ^ZcJ*}W*9X`{cix)?@4i&WiR%vXJ%)3`MG-oD3< zcR|*zKBXx`WF3dcE&IshiZyj*vwxkI85c)Z8=?l)1zB?Tr$t5KaWZ7)ETel#6r_~Z zULjcDDgo46h_6G@kO{5)3Z4R7S$JD^Mtx&lC7g?uYDNP;qW{E7VI}!&$pX&nKTgX+ zv#Kt3;$6oVt2zo?%%4)>QrF{ywFLDoC&Aaz-o8FnsX`3rsSK^k$~Z+XlZ3o~kmQC) zpFy6r7(Cj&5l=Ynb38YCl=`C9xp(M5|7W zy!B^|EQIyjTxpPNKv&%63QH5xEp2>qiQl}E1e&SpIO#fS#}L*A5OLW0jIX+6H?w2f zur7^x+$c?Ajb?cmTHEiC5h3=rGK-vAmd1!d%x3ug>KUhr>QZ}3>6big+FD1JSKcfD z`z=L0={gbo-|ud8m`8Wu{{NCY+5eunoc{?5(0EqZ$+NGQvteI{%}J)Ow{P0p|Loq( zLCUt$_ftOjohV6T063#Dz&3-!eMwRXu4`W1u$>HsMY|r}Y~L)YDZl*lIqd1?i?za= z<`-$L52bBx&NP3EvxbLV*N{?dsmo1B9t;_UJdPL^I85nDxc(Iv)S>PR`SY3e<0=2g z>9c1^4#LH=bPAnjZkS}+FMxi(y29aq76v7ILE17Ze-re72y|EcNv?6*oTw35aNZ^h;ZnQ1BG1tzrvh`cWo* z7WX@bU&hzclaA(pmD-#IDQSZ8c|%ez*r`^n0~{#Y9KSlM0RcKbH4#7`N~HBtJ)9NX zOl|M#Rec~n6Ps1h&rn1APDAXqIkNo1(@Nx>xH>G6_uD%ahc;L_2*9grj?>bni=V1j zI#|$Q@>?jAm@(A`5T-BjpLO9tZ-X6==C^W~2ZmQzcbl!s3bDV1tLPZAdju`}-6yPl zFk5=PZ!$fj2F79@mU`li*e-3UNCjXAX%x$~EJ(xG}0QB1TRXUAB1fYd5 zC6063vI)vmIAT(!6>uakt-K{saDw79@ZNX{!>4buw4yTn6-*M9-nChZvTiy?V2Vzo zp|u4UM-^Ke=>|B^3-G;)h=OnK4+NchS!8oUad)kpyL7Ni1s~c;+6cKcIUG#asNoI2qY|~0VzVSKXUAqnD zF*A&oO5T`T{F3@ke&B~7qmqpic|sb|s^A9f;0vRn+AECKejb0vM~P{e@ef#8il#S4 zb@sYR#_24-Yjp6^mW8J(fI%%cff=7dbM>G<_Xj4sIT}@0aN&(wH>wt^E7)Yx11EXQ ztn$7iXnka?Ll3H$bki!F+{k)m^NPZ?46Ms@LRS4PxRN$kx{*EVjl=@^Ea~|eQ@_kE zwodzk#}z2&+TlF?lqXQP^$-u7;liD00}lm8sQ^z?x~J8g%H6maaGam?Y%k~(H>O12-DP?r(r#C#-s!$@##VwqlkzTeh$Ul6V-D0C^~$svKS2V~Bq8Kn%lf&4Iv6+k#pmvoh>F3>EGrTHi)$s!p?*(s) z8DhHx=59i8w&L#Q7q4~Zt7k?9e^qa@VdmWap%)y0BOoKTS6bcFLbkneboeFib4zTh zydm98G6$75OFS8X5apGXJOsIlwq90e!&p2TZP?;6Q$z*?Cs}@V6|JY}eBmrCf2q~( zJWWQl-6v<5`)94VlP9)oaK%2zjC3m$g9AIY_0)cGMrmjRy&VKKfhFGO#!tN;Hy0+~ zh)z%@AOz^mPUG^>=*gosZ$HZw)VQe6LM$3JY4Q-bzIPGMR5cZjLr}PyLc6o(PF2i^ zGR_6|g{UV5q^_^~D6DH>&tH9oXtv){W0?GHc;54Tius)Hg1y4hBTN>B^b#npyESIi zz)ympPc2#v@Ln%7uY=T2>ONg&^d9_O){nLpWlB*Wh;pv|myG&xd_<5DI62@n>uZuy zu5r+jnCAwWl&yWA9OWK52;1vn2yU=SoNKo zZ%l=TmEbi~=S1$?b^{Q*I`dAe;LVh;k%s6cXf)Vq{*A6la(~|B z{qy_(4>kP%r>Ri?QSSeL@lii+Ks|Ikq?R#ux!O1&Cl2(kzqmYcq8xonIR0e$Xr1+@ z1(Q%82yY&1_vKv$YX8)~KHkP*xy-C5i@f;kQpoEHv;n&<{IUyZ5PVnv9cv334Deys zh(dlgZktNA3rfxFxMW|DgVjZ*_sS+;{<+*X_D#CGHpRIW?{Uvmw&^!fhDVC- z&p#73NZTv49wjtX7?D zx+Q{JhdJ&=jCI$TiAn5-!Yuuc&(G;*to%#RZ*h_1Nhixt8yoGyZad>i` z-SXZiDeZs{^x#cgACpZGu2D_AH#IHVD1+haV)?fiIY{6Lk%+e$7}?4@#)a0{aH9xi zfh>E6Y<(G+hikNSHU)hq;c;cW{HACBTY&OwWbNGdR}7zuZ4-Z>zTwW@5jDMQSsxV6 zi+ZG8xB0m&#t@J^EVTD+!s^J;Cj92Sx?JYAY5=hZn7Blmx~XKOkcli`kle;$=TInb z)13Xg3A1Xswf+7@(A~^iWUbil{*_*GVJjj9s#d^GENb=bIrl8Q%w)Aqx?fR%lJh(N zX`5Poh`l=0Z4!mf5gz9m50t~_DdAr9lm#ffgaS?vO*o#LzCe=`Y~BAVxFo1Gmi;2* zdxTf71G-*vb!Ks(f4OToYq3#lsiW5c;`5`3{4?>@CBwIFU;>r}>f`2DA3d5CY2<8l z;Idu5NXOM{Ll_5`PT8v6Y+`LJu+Gp)@vQ zE*AT38ylm+>NG3V|N4>IJsb{?#@q-7+$8*S9QNdmC6xvyRv2R1l}rF%P}j)9vzG8; zcQ!hKj_Mh&reh>s#Za>ynF+C4!H1hgO3Wl1qgi_rWd^|~Hrh`GmuCT}rA?PZmna~r z-Rd@K_4`118}gDeh^M`qPK$S^AW+!eCdNqIR-A^)j5DzmTGk@sq+ygrDiPHuo2OFr zI+BaDjWu<#Vt}ukILQmEfrsOx7KUL_93IaypOc%4DDx%_q#6a#YK(Z?$^9K5tR)MG zTO}elUD=0l+8E~EST+h!vRuRK+CG5H`keV6UUt3bsc@q7ddEAeEnpv?A=8WV)K`~> zpEPpep(u>ef4t7bX2}yiV%OY7(Fi1Ku}7v1CE7x}20=U_o6PFUobTJ#3o{Xdo&1j^ z*&|p?ovE%#(z{)4YFmaM>#IQ;=#tBzIJ4Fvt%=?Jbg33g^JM)17)P51M_$Zr;r1Es zt#-q~Mq1vDK3gO7&NNHrY2o{oIQZkAAA?n*FC2DQ)pJL}8ur6mxu11UOP3yOHFuuIa z%wz{5+6L1q^1p&>*JIq;GS;AWJ*jUW&BHt(a8 zE+GTRb7adaYuX0hs()HUnJ5ep#_@n689KzG+Xw7f149&AJJ}S60;z%+EVH ztnMOY&U>xIE6**nKPkXcv(uy6Ka7*|(2`<3-#VIiQ8gj<(Bvad+_7_kr;3adqWZWja^3n&x8z;5zNeAe-KmizO{e^#&(Gi>a~p7&+^3Hp{Gp`Oz_t%>4Z_)i}}UfW8?d-`WCOO zg?Zp5cg;s%*`&6HIV6l%+%EFawL4f|+El36D7mRGCD)7hl&OeP0)F`l2D-{Nk8do2 zshhHU+&^WO_@`+mc`@636Jur8l+pw?Z88CVp!o3*3~rC#3HLdTJkR07)H~k+#{0Ri@$!fGH2~_@30v%g-54LD#e8uS@#?GCOKU2h4UN|?W@X*K zHq@6f3Mma){H)5*DDV`mdB>*~^G<;xMnNpsm!jfBuXlQggW_F)GOjGBT!hX3I@Xue zq<}kygguE)V)p4y>EXX-?Nr(%*@yd%qb%c|S2HYLV@44dmWBGeyO|ZY(b>jex|d zddbV1gj28OLDc7dh-`DiAm2`<(vCO2(N%I8FfOglMbAsgF173l#&Xhq#@g`v{%fvA z^?33BQkI*1uRvQw0BQ8%TDD(PJ*e-{goz}vR=IKio|XZ@HKH>!>vyEo6rw*fOod%dcP<;xAs?fHw?G=Tt6tA z*bU7dOby1~27#8?Ybs`oM4OGwD7_|D641sgjV>U&)kzR1qu6v!A_D98{uA~IsKf?BI5M2s&g$^Z`46!>K@s8hm@K@=k zQ>usM0{)?ux><`zVqt#}?ov{_sQm_D#ML_$h}-uDys(CsEW)$b8?@{U4f7a1afy@Q z69<8q(L(zh7}pC2=h9v+1}z7skF$Na1^N))-i`pAEK8}0&m`XF zwXCsH$#Z`WndGc1q6Go{oIrZd17lbsA055v0mpg&E3JzX`K3iF;z!11uQ}fxp7QOs zQfSRA=f?@8y;J#Qo8k*a$QlW@E@_cyILzAeP3RD)3H{as@F}f$e zfw+Z-xZS+FIG%;$pxklKi24io4v7HUa}yp7t11MoQzHbXJ{Lt#toI>nTZl?^3BLhvh%jY|!?sJmC1>`;dbz1tHW(wK#)xt!0 zCY2~9Gt5QcgwYv(ZRD%B*Ia=diR}kKl<%}_w;V{ZumRpd!}0j~r#L@!7{v=VQ@uX! zLpFJ_$NZEeTJv))q;7?NY zy-k+GSv!*x<0~1yWFWWtvW8KIK>PQlWJB;zo zM1!|Rk%Rv|0$(@&+-YiD>iqDEovBCKCmbD6^dmn{RQUC_DB}bZP1IBjeKupoZ zWYan=`@Fd&nFD@WcFDYz*&s}U03GZWd@oNjgVPn43*4AxW*Li!|AlZLsN?9Zvv!uL zAw=|ZmQuCEQYv`iMpzNf)47dwG+#6rByi5yX=DL8%7?l{1jV^AjY%w8-re72y2Q}< zA-gzI)?NG|#d10$te+1DRNtbtDwjK3#}V78Y-+}AV_Sb;9f#rTCWZHcYrKOLma70w zmXOku-b?BwBRZ=1d}rcdJUj+9-6Hd#hG)pRlQllYd77A7{1fa31Yf`XY$L$gzhB32=2 zq-Yc3QG5NZt#_-rFL6Erh3N5hK_}2%`3mjLkW2}aVQ?icQd@+!sazN&`R>FX6YSV_ zxjNuz>-yArsP#4EEmYE#$>(~5OMoxyVLFFimJ~`E1QU><+1#$*p zR2HaKy75k;!d%f=WdAC6bVd^3tvQw+0)r+r^SyDG-QV0xM(htdY`E2sqYtNNg68lZ zL{o^hAcTrj**UbK&rW*RQWHy==po#u>eA2A#a%}k{lV=gzmdqjx4sl9K;KHO5^zy( z3%1i_*~?aEfaS503B>=rdFvaWvt;NcIm(A45i?j#WadB(Yo8h1>dQj|dYYtdnF zX`7i)NS7P!rtKB}Cv}1ySUfv%+|ABxmh;rKE(7eXb@SOn-ZaTC zJHGQzy>Xw~ud;nFT)Qx9F*@UWP9>mk`SiZH$uGe{TN{ebVAt*qYGiCPBq_MdHFGw( zt{8ftS^%c8CrUDjG<||T{WNFox z1Cm=7T3Q|RM`=#_-3_~WzH-KSF9qcsNxn1w^Kn+OMc5OAC#23~Rs#p4+28sDJssWy zW9zT5uQ+hb$4;LYsF*fcKN32S}R54xRQR4*B={vrl_5pY5DZ;m1=b{8PN& u;P6UkAl@UT=)cGX{|kq5NPI_~-r%IP*={%S4TWO?e;%motG!dPjr?yV*~;bs literal 0 HcmV?d00001 From 5015cb40bf9d0bb88ad2de5ceaf9afbd58d76b10 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Fri, 12 Apr 2024 19:03:47 +0200 Subject: [PATCH 13/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Code Class Diagram Documentation Update --- .../Connection/__ConnectionClassDiagram.cd | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd b/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd index dfd27922..bbd032d3 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd @@ -17,7 +17,7 @@ - AABAAIAAAAKAACAAiAACAAAAAAhAAAMAAAAIAAAFBAA= + AABAAIAAAAKAACAAiAACAABAAAhAAAMAAAAIAAAFBAA= Connection\InfoShareOpenIdConnectConnectionParameters.cs @@ -39,13 +39,13 @@ - AAAAAAAAAAACAAAAAgAAAAABKAAAAAAQAAAAAAAAAAA= + AAAAAAAAAAACAAAAAgAAAABBKAAAAAAQAAAAAAAAAAA= Connection\InfoShareOpenIdConnectSystemBrowser.cs - + - cBgEBIhQ0WAAAJAgQMsQAMGAgBWBxiAA8AQYBgiECog= + cBgEBIxQ0WAAAJAgQMMQAIGAgBWBxiAA8AQYBgiECog= Connection\InfoShareWcfSoapWithOpenIdConnectConnection.cs - + - cBgFVIhQ0WAAQJAgQNsQAJGAsRWBxiBCcAQYBgiECog= + cBgFVIhQ0WAAQJAgQNMQAJGAsRWBxiBCcAQYBgiECog= Connection\InfoShareWcfSoapWithWsTrustConnection.cs @@ -144,14 +144,14 @@ - AAAAEAAAAQAAIAAAABAQAAAAIAAAAAAAAAAAAAAAAAA= + AAAAEAAAAQAAMAAAABAQAEAAIAEAAAAAAAAAAAAAAAA= Connection\InfoShareOpenIdConnectConnectionBase.cs - + - AAAAAAAAACAAAAAAAA0AAEAAAAAAAAAAAAAAAgAAAAA= + AAQAAAAAACAAAAAAAAAAAAAAAgAAAAAEAQAAAgAAAAA= Connection\InfoShareOpenApiWithOpenIdConnectConnection.cs From 807281d05037a4cb61d7270e3e38dc601979f4b2 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Tue, 16 Apr 2024 14:48:53 +0200 Subject: [PATCH 14/14] #180 Add IShSession.OpenApiAM10Service proxy similar to IShSession.OpenApiISH30Service proxy... Extended AppDomainModuleAssemblyInitializer to force load Microsoft.Extensions.Logging for otokar's PowerShell_ISE.exe (PS5.1) --- Doc/ReleaseNotes-ISHRemote-8.0.md | 1 + .../Trisoft.ISHRemote/Cmdlets/Session/SessionCmdlet.cs | 2 +- .../HelperClasses/AppDomainModuleAssemblyInitializer.cs | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Doc/ReleaseNotes-ISHRemote-8.0.md b/Doc/ReleaseNotes-ISHRemote-8.0.md index 6de155d0..107a8e25 100644 --- a/Doc/ReleaseNotes-ISHRemote-8.0.md +++ b/Doc/ReleaseNotes-ISHRemote-8.0.md @@ -132,6 +132,7 @@ Bcl.AsyncInterfaces.dll/System.Text.Encodings.Web.dll |PS5.1/NET4.8.1|System.Text.Encodings.Web, Version=5.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51|{System.Text.Encodings.Web, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51}| |PS5.1/NET4.8.1|_Only on Github Actions container, extended AppDomainModuleAssemblyInitializer to resolve CI/CD issues_|{System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51}| |PS5.1/NET4.8.1|System.ComponentModel.Annotations, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a _for NET48/OpenApi clients_|System.ComponentModel.Annotations, Version=4.2.1.0, Culture=neutral| +|PS5.1/NET4.8.1|Microsoft.Extensions.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60|Microsoft.Extensions.Logging, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60| |PS7.3.6/NET6.0|IdentityModel, Version=6.1.0.0, Culture=neutral, PublicKeyToken=e7877f4675df049f|{IdentityModel, Version=6.1.0.0, Culture=neutral, PublicKeyToken=e7877f4675df049f}| ## Known Issues diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/SessionCmdlet.cs b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/SessionCmdlet.cs index b4276f07..f0232b29 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/SessionCmdlet.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/Session/SessionCmdlet.cs @@ -40,7 +40,7 @@ protected override void BeginProcessing() { base.BeginProcessing(); #if NET48 - WriteVerbose("ISHRemote module on PS5.1/NET48 forces Assembly Redirects for System.Runtime.CompilerServices.Unsafe.dll/System.Text.Json.dll/IdentityModel.OidcClient.dll/Microsoft.Bcl.AsyncInterfaces.dll/System.Text.Encodings.Web.dll"); + WriteVerbose("ISHRemote module on PS5.1/NET48 forces Assembly Redirects for System.Runtime.CompilerServices.Unsafe.dll/System.Text.Json.dll/IdentityModel.OidcClient.dll/Microsoft.Bcl.AsyncInterfaces.dll/System.Text.Encodings.Web.dll/System.Memory.dll/System.ComponentModel.Annotations.dll/Microsoft.Extensions.Logging.dll"); #else WriteVerbose("ISHRemote module on PS7.2+/NET60+ forces Assembly Redirects for IdentityModel.dll"); #endif diff --git a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs index dac1293f..c1d909f0 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/HelperClasses/AppDomainModuleAssemblyInitializer.cs @@ -51,6 +51,7 @@ namespace Trisoft.ISHRemote.HelperClasses /// * IdentityModel.OidcClient requested but we now return /// * Microsoft.Bcl.AsyncInterfaces requested 5.0.0.0 but we now return 6.0.0.0 /// * System.ComponentModel.Annotations requested 4.2.0.0 but we now return 4.2.1.0 (for NET48/OpenApi clients) + /// * ...see code below /// /// Focus was to getting this working on NETFramework, more implementation is required to align with /// proposed solution of https://devblogs.microsoft.com/powershell/resolving-powershell-module-assembly-dependency-conflicts/ @@ -99,6 +100,10 @@ public void OnImport() filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"System.ComponentModel.Annotations.dll"); assembly = Assembly.LoadFrom(filePath); _forcedLoadedAssemblies.GetOrAdd("System.ComponentModel.Annotations", assembly); + + filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"Microsoft.Extensions.Logging.dll"); + assembly = Assembly.LoadFrom(filePath); + _forcedLoadedAssemblies.GetOrAdd("Microsoft.Extensions.Logging", assembly); #else AssemblyLoadContext.Default.Resolving += ResolveAssembly_NetCore;