From 04a82883962136eced41603c09e56c5f8bf8b050 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:10:45 +0500 Subject: [PATCH] cd: #493: add CORS settings to allow call API requests from the local UI but for prod use strict settings limited to only prod domain and only used headers and method types --- .devcontainer/devcontainer.json | 1 + .../.reusable-e2e-tests-against-prod.yml | 1 + .../workflows/deploy-to-prod-from-default.yml | 1 + .../workflows/e2e-tests-on-pull-request.yml | 1 + Api/CorsOptions.cs | 6 ++++ Api/Program.cs | 13 ++++---- Api/appsettings.MockForDevelopment.json | 3 ++ Api/appsettings.MockForPullRequest.json | 3 ++ Api/ci/helmfile.yaml | 1 + docker-compose.yml | 1 + e2e/cors-settings.feature | 33 +++++++++++++++++++ 11 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 Api/CorsOptions.cs create mode 100644 e2e/cors-settings.feature diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b87231c..8caa378 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,5 +24,6 @@ }, "containerEnv": { "API_ROOT_URL": "http://localhost:6503/api", + "CORS_ALLOWED_ORIGINS": "*" } } \ No newline at end of file diff --git a/.github/workflows/.reusable-e2e-tests-against-prod.yml b/.github/workflows/.reusable-e2e-tests-against-prod.yml index d487f66..c5013d6 100644 --- a/.github/workflows/.reusable-e2e-tests-against-prod.yml +++ b/.github/workflows/.reusable-e2e-tests-against-prod.yml @@ -32,4 +32,5 @@ jobs: "AUTH_FIRST_TENANT_LOGIN_WITH_ALL_PERMISSIONS": ${{ secrets.INNER_CIRCLE_PROD_AUTH_FIRST_TENANT_LOGIN_WITH_ALL_PERMISSIONS }} "AUTH_FIRST_TENANT_PASSWORD_WITH_ALL_PERMISSIONS": ${{ secrets.INNER_CIRCLE_PROD_AUTH_FIRST_TENANT_PASSWORD_WITH_ALL_PERMISSIONS }} "API_ROOT_URL": ${{ secrets.INNER_CIRCLE_PROD_AUTH_API_ROOT_URL }} + "CORS_ALLOWED_ORIGINS": ${{ secrets.INNER_CIRCLE_PROD_BASE_URL }} "SHOULD_USE_FAKE_EXTERNAL_DEPENDENCIES": "false" \ No newline at end of file diff --git a/.github/workflows/deploy-to-prod-from-default.yml b/.github/workflows/deploy-to-prod-from-default.yml index 3a02cdd..89109d9 100644 --- a/.github/workflows/deploy-to-prod-from-default.yml +++ b/.github/workflows/deploy-to-prod-from-default.yml @@ -30,6 +30,7 @@ jobs: --state-values-set extraSecretEnvVars.InnerCircleServiceUrls__MailServiceUrl="${{ secrets.INNER_CIRCLE_PROD_MAIL_SERVICE_URL }}" \ --state-values-set extraSecretEnvVars.InnerCircleServiceUrls__EmployeesServiceUrl="${{ secrets.INNER_CIRCLE_PROD_EMPLOYEES_SERVICE_URL }}" \ --state-values-set extraSecretEnvVars.AuthenticationOptions__PublicSigningKey="${{ secrets.INNER_CIRCLE_PROD_PUBLIC_SIGNING_KEY }}" \ + --state-values-set extraSecretEnvVars.CorsOptions__AllowedOrigins="${{ secrets.INNER_CIRCLE_PROD_BASE_URL }}" \ --state-values-set extraSecretEnvVars.AuthenticationOptions__PrivateSigningKey="${{ secrets.INNER_CIRCLE_PROD_PRIVATE_SIGNING_KEY }}" > "/var/log/inner-circle/deploy/$(date +%F)-${{ github.event.repository.name }}-run-${{ github.run_id }}-job-${{ job.check_run_id }}.log" 2>&1 # on the last line where we redirect output we don't add a line break for convenience because it is easy to make it wrong having an extra space after \ symbol which leads to exposure of logs to the public pipeline logs instead of the needed private file on a runner diff --git a/.github/workflows/e2e-tests-on-pull-request.yml b/.github/workflows/e2e-tests-on-pull-request.yml index 6c85f65..64f2c93 100644 --- a/.github/workflows/e2e-tests-on-pull-request.yml +++ b/.github/workflows/e2e-tests-on-pull-request.yml @@ -60,4 +60,5 @@ jobs: "AUTH_FIRST_TENANT_PASSWORD_WITH_ALL_PERMISSIONS": "Serpens1!" "AUTH_API_ROOT_URL": "http://localhost:30090/api/auth" "API_ROOT_URL": "http://localhost:30090/api/auth" + "CORS_ALLOWED_ORIGINS": "*" "SHOULD_USE_FAKE_EXTERNAL_DEPENDENCIES": "false" diff --git a/Api/CorsOptions.cs b/Api/CorsOptions.cs new file mode 100644 index 0000000..3886f1d --- /dev/null +++ b/Api/CorsOptions.cs @@ -0,0 +1,6 @@ +namespace Api; + +public class CorsOptions +{ + public required string AllowedOrigins { get; set; } +} \ No newline at end of file diff --git a/Api/Program.cs b/Api/Program.cs index 841e558..d2c1f6f 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; +using Api; using Api.Services; using Api.Services.Callbacks; using Api.Services.Options; @@ -21,7 +22,6 @@ const string defaultConnection = "DefaultConnection"; builder.Services.AddControllers(); -builder.Services.AddCors(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -92,7 +92,7 @@ .AddRefreshConfidenceInterval() .AddLogout() .AddUserCredentialsValidator() - .WithUserClaimsProvider(UserClaimsProvider.PermissionsClaimType); + .WithUserClaimsProvider(Api.Services.Users.UserClaimsProvider.PermissionsClaimType); builder.Services.AddIdentityCore().AddDefaultTokenProviders(); @@ -127,12 +127,13 @@ var app = builder.Build(); +var corsOptions = configuration.GetSection(nameof(CorsOptions)).Get(); + app.UseCors( corsPolicyBuilder => corsPolicyBuilder - .AllowAnyHeader() - .SetIsOriginAllowed(host => true) - .AllowAnyMethod() - .AllowAnyOrigin() + .WithOrigins(corsOptions!.AllowedOrigins) + .WithMethods("GET", "POST", "DELETE") + .WithHeaders("Authorization", "Content-Type") ); if (app.Environment.IsEnvironment("Debug")) diff --git a/Api/appsettings.MockForDevelopment.json b/Api/appsettings.MockForDevelopment.json index 75c0622..4ebf9e7 100644 --- a/Api/appsettings.MockForDevelopment.json +++ b/Api/appsettings.MockForDevelopment.json @@ -12,5 +12,8 @@ "AuthUIServiceUrl": "https://localhost:3000", "AccountsServiceUrl": "http://localhost:5001", "EmployeesServiceUrl": "http://localhost:5006" + }, + "CorsOptions": { + "AllowedOrigins": "*" } } \ No newline at end of file diff --git a/Api/appsettings.MockForPullRequest.json b/Api/appsettings.MockForPullRequest.json index ce12678..8cf03e4 100644 --- a/Api/appsettings.MockForPullRequest.json +++ b/Api/appsettings.MockForPullRequest.json @@ -12,5 +12,8 @@ "AuthUIServiceUrl": "https://localhost:3000", "AccountsServiceUrl": "http://auth-api-mock-server:1080", "EmployeesServiceUrl": "http://auth-api-mock-server:1080" + }, + "CorsOptions": { + "AllowedOrigins": "*" } } \ No newline at end of file diff --git a/Api/ci/helmfile.yaml b/Api/ci/helmfile.yaml index 8d8706a..737bf3d 100644 --- a/Api/ci/helmfile.yaml +++ b/Api/ci/helmfile.yaml @@ -22,4 +22,5 @@ releases: InnerCircleServiceUrls__MailServiceUrl: "{{ .StateValues.extraSecretEnvVars.InnerCircleServiceUrls__MailServiceUrl }}" InnerCircleServiceUrls__AuthUIServiceUrl: "{{ .StateValues.extraSecretEnvVars.InnerCircleServiceUrls__AuthUIServiceUrl }}" InnerCircleServiceUrls__AccountsServiceUrl: "{{ .StateValues.extraSecretEnvVars.InnerCircleServiceUrls__AccountsServiceUrl }}" + CorsOptions__AllowedOrigins: "{{ .StateValues.extraSecretEnvVars.CorsOptions__AllowedOrigins }}" InnerCircleServiceUrls__EmployeesServiceUrl: "{{ .StateValues.extraSecretEnvVars.InnerCircleServiceUrls__EmployeesServiceUrl }}" diff --git a/docker-compose.yml b/docker-compose.yml index e069a70..bb3b829 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,7 @@ services: API_ROOT_URL: "http://auth-api/api" AUTH_LOGIN: ${TEST_AUTH_LOGIN} AUTH_PASSWORD: ${TEST_AUTH_PASSWORD} + CORS_ALLOWED_ORIGINS: "*" networks: - auth-api-network diff --git a/e2e/cors-settings.feature b/e2e/cors-settings.feature new file mode 100644 index 0000000..58db883 --- /dev/null +++ b/e2e/cors-settings.feature @@ -0,0 +1,33 @@ +Feature: CORS Settings + # https://github.com/karatelabs/karate/issues/1191 + # https://github.com/karatelabs/karate?tab=readme-ov-file#karate-fork + + Background: + * header Content-Type = 'application/json' + + Scenario: Verify API CORS settings + + * def jsUtils = read('./js-utils.js') + * def authApiRootUrl = jsUtils().getEnvVariable('AUTH_API_ROOT_URL') + * def apiRootUrl = jsUtils().getEnvVariable('API_ROOT_URL') + * def authLogin = jsUtils().getEnvVariable('AUTH_FIRST_TENANT_LOGIN_WITH_ALL_PERMISSIONS') + * def authPassword = jsUtils().getEnvVariable('AUTH_FIRST_TENANT_PASSWORD_WITH_ALL_PERMISSIONS') + * def corsAllowedOrigins = jsUtils().getEnvVariable('CORS_ALLOWED_ORIGINS') + + # Send CORS preflight OPTIONS request like UI does + Given url authApiRootUrl + And path '/login' + And request + """ + { + "login": "#(authLogin)", + "password": "#(authPassword)" + } + """ + And header Origin = corsAllowedOrigins + And header Access-Control-Request-Method = 'POST' + When method OPTIONS + Then status 204 + And match responseHeaders["Access-Control-Allow-Origin"] == ["#(corsAllowedOrigins)"] + And match responseHeaders["Access-Control-Allow-Methods"] == ["GET,POST,DELETE"] + And match responseHeaders["Access-Control-Allow-Headers"] == ["Authorization,Content-Type"] \ No newline at end of file