diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml index 9eb0b10d4..2165aa47b 100644 --- a/.github/workflows/test-install.yml +++ b/.github/workflows/test-install.yml @@ -21,23 +21,16 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # ubuntu18 was unstable at github (2022-07-06 - 2022-07-11) - # test_ubuntu_18: - # name: test build on ubuntu_18 - # runs-on: ubuntu-18.04 + # # ubuntu18 was unstable at github (2022-07-06 - 2022-07-11) + # # does not seem to be supported by hithub anymore (2024-05-01) + + # test_ubuntu_20: + # name: test build on ubuntu_20 + # runs-on: ubuntu-20.04 # steps: # - uses: actions/checkout@v3 # - name: do test install in case of merged pull request - # run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e run_on_github=yes --skip-tags test site.yml -K - - test_ubuntu_20: - name: test build on ubuntu_20 - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - name: do test install in case of merged pull request - run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e run_on_github=yes site.yml -K -# run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e run_on_github=yes --skip-tags test site.yml -K + # run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e force_install=true site.yml -K # test_ubuntu_22: # name: test build on ubuntu_22 @@ -45,5 +38,13 @@ jobs: # steps: # - uses: actions/checkout@v3 # - name: do test install in case of merged pull request - # run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e run_on_github=yes site.yml -K - # run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e run_on_github=yes --skip-tags test site.yml -K + # run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e force_install=true site.yml -K + + test_ubuntu_latest: + name: test build on ubuntu latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: do test install in case of merged pull request + run: cd /home/runner/work/firewall-orchestrator/firewall-orchestrator && ansible-playbook -e force_install=true site.yml -K + diff --git a/documentation/revision-history-develop.md b/documentation/revision-history-develop.md index 0bb70508e..df39a94e6 100644 --- a/documentation/revision-history-develop.md +++ b/documentation/revision-history-develop.md @@ -202,3 +202,6 @@ bugfix release: - fix demo managements (change import from deactivated to activated - does not affect test managements) - upgrade to dotnet 8.0 - adding all imported modelling users to uiuser + +# 8.2.1 - xx.05.2024 DEVELOP +- fix misleading login error message when authorisation is missing diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index 80fd8cfe8..19c999f6b 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -1,5 +1,5 @@ ### general settings -product_version: "8.2" +product_version: "8.2.1" ansible_user: "{{ lookup('env', 'USER') }}" ansible_become_method: sudo ansible_python_interpreter: /usr/bin/python3 @@ -22,7 +22,6 @@ sample_hostname: "{{ groups['sampleserver'].0 }}" # upgrade - installs on top of an existing system preserving any existing data in ldap, database, api installation_mode: new install_syslog: true -run_on_github: false add_demo_data: true api_docu: false force_install: false diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 72f6eacad..5ec90ac8b 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,11 +1,10 @@ - block: - - name: assert ansible version gt 2.13 + - name: assert ansible version gt 2.12 fail: msg: Ansible 2.13 or above is required when: ansible_version.full is version('2.13', '<') - - name: check for existing main config file {{ fworch_conf_file }} stat: path: "{{ fworch_conf_file }}" @@ -93,23 +92,10 @@ - There are upgradable OS packages available, please run OS upgrade before running FWORCH installer. - Use "-e force_install=true" to overwrite this check and install anyway at your own risk. when: | - not force_install|bool and not run_on_github|bool and + not force_install|bool and (ansible_facts['distribution'] == "Ubuntu" or ansible_facts['distribution'] == "Debian") and upgradable_packages.stdout_lines|length > 1 - - # - name: fix grub-efi (for github actions) - # apt: - # upgrade: dist - # update_cache: true - # when: ansible_facts['distribution'] == "Ubuntu" or ansible_facts['distribution'] == "Debian" and run_on_github|bool - - # - name: update operating system packages .deb based (for github actions) - # apt: - # upgrade: dist - # update_cache: true - # when: ansible_facts['distribution'] == "Ubuntu" or ansible_facts['distribution'] == "Debian" and run_on_github|bool - - name: update operating system packages .rpm based (untested) yum: upgrade: dist diff --git a/roles/database/files/sql/idempotent/fworch-texts.sql b/roles/database/files/sql/idempotent/fworch-texts.sql index 47eaf9131..488db5e9f 100644 --- a/roles/database/files/sql/idempotent/fworch-texts.sql +++ b/roles/database/files/sql/idempotent/fworch-texts.sql @@ -273,6 +273,8 @@ INSERT INTO txt VALUES ('permissions_text', 'German', 'Ihre Berechtigungen wur INSERT INTO txt VALUES ('permissions_text', 'English', 'Your permissions have been changed. Re-login to update your permissions.'); INSERT INTO txt VALUES ('login_importer_error', 'German', 'Nutzer mit der Rolle "Importer" dürfen sich nicht an der Benutzeroberfläche anmelden. Diese Rolle dient einzig dem Importieren von eingebundenen Geräten.'); INSERT INTO txt VALUES ('login_importer_error', 'English', 'Users with role "importer" are not allowed to log into the user interface. The only purpose of this role is to import included devices.'); +INSERT INTO txt VALUES ('not_authorized', 'German', 'Authentisierung OK, aber keine Berechtigung/Authorisierung vorhanden.'); +INSERT INTO txt VALUES ('not_authorized', 'English', 'Authentication succeeded, but not authorized.'); -- navigation INSERT INTO txt VALUES ('reporting', 'German', 'Reporting'); diff --git a/roles/test/tasks/main.yml b/roles/test/tasks/main.yml index 526b04450..a02f13b80 100644 --- a/roles/test/tasks/main.yml +++ b/roles/test/tasks/main.yml @@ -62,7 +62,6 @@ - name: auth testing import_tasks: test-auth.yml - when: "not run_on_github|bool" - name: api testing import_tasks: test-api.yml diff --git a/roles/test/tasks/test-auth.yml b/roles/test/tasks/test-auth.yml index dd400917c..b91352757 100644 --- a/roles/test/tasks/test-auth.yml +++ b/roles/test/tasks/test-auth.yml @@ -7,7 +7,6 @@ connect_timeout: 1 delay: 10 timeout: 25 - when: "not run_on_github|bool" - name: middleware test get jwt valid creds uri: diff --git a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs index d2246234e..5457f5151 100644 --- a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs +++ b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs @@ -1,11 +1,8 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Authorization; using FWO.Config.Api; using FWO.Api.Client; -using FWO.Api.Client.Queries; -using FWO.GlobalConstants; using FWO.Api.Data; using FWO.Ui.Services; using FWO.Middleware.Client; @@ -15,201 +12,205 @@ using FWO.Logging; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using System.Security.Authentication; -using System.Security.Principal; - namespace FWO.Ui.Auth { - public class AuthStateProvider : AuthenticationStateProvider - { - private ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity()); - - public override Task GetAuthenticationStateAsync() - { - return Task.FromResult(new AuthenticationState(user)); - } - - public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, - GlobalConfig globalConfig, UserConfig userConfig, ProtectedSessionStorage sessionStorage, CircuitHandlerService circuitHandler) - { - // There is no jwt in session storage. Get one from auth module. - AuthenticationTokenGetParameters authenticationParameters = new AuthenticationTokenGetParameters { Username = username, Password = password }; - RestResponse apiAuthResponse = await middlewareClient.AuthenticateUser(authenticationParameters); - - if (apiAuthResponse.StatusCode == HttpStatusCode.OK) - { - string jwtString = apiAuthResponse.Data ?? throw new Exception("no response data"); - await Authenticate(jwtString, apiConnection, middlewareClient, globalConfig, userConfig, circuitHandler, sessionStorage); - Log.WriteAudit("AuthenticateUser", $"user {username} successfully authenticated"); - } - - return apiAuthResponse; - } - - public async Task Authenticate(string jwtString, ApiConnection apiConnection, MiddlewareClient middlewareClient, - GlobalConfig globalConfig, UserConfig userConfig, CircuitHandlerService circuitHandler, ProtectedSessionStorage sessionStorage) - { - // Try to auth with jwt (validates it and creates user context on UI side). - JwtReader jwtReader = new JwtReader(jwtString); - - if (await jwtReader.Validate()) - { - // importer is not allowed to login - if (jwtReader.ContainsRole(Roles.Importer)) - { - throw new AuthenticationException("login_importer_error"); - } - - // Save jwt in session storage. - await sessionStorage.SetAsync("jwt", jwtString); - - // Tell api connection to use jwt as authentication - apiConnection.SetAuthHeader(jwtString); - - // Tell middleware connection to use jwt as authentication - middlewareClient.SetAuthenticationToken(jwtString); - - // Set user claims based on the jwt claims - ClaimsIdentity identity = new ClaimsIdentity - ( - claims: jwtReader.GetClaims(), - authenticationType: "ldap", - nameType: JwtRegisteredClaimNames.UniqueName, - roleType: "role" - ); - - // Set user information - user = new ClaimsPrincipal(identity); - string userDn = user.FindFirstValue("x-hasura-uuid"); - await userConfig.SetUserInformation(userDn, apiConnection); - userConfig.User.Jwt = jwtString; - userConfig.User.Tenant = await getTenantFromJwt(userConfig.User.Jwt, apiConnection); - userConfig.User.Roles = await getAllowedRoles(userConfig.User.Jwt); - userConfig.User.Ownerships = await getAssignedOwners(userConfig.User.Jwt); - circuitHandler.User = userConfig.User; - - // Add jwt expiry timer - JwtEventService.AddJwtTimers(userDn, (int)jwtReader.TimeUntilExpiry().TotalMilliseconds, 1000 * 60 * globalConfig.SessionTimeoutNoticePeriod); - - if (!userConfig.User.PasswordMustBeChanged) - { - NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); - } - } - else - { - Deauthenticate(); - } - } - - public void Deauthenticate() - { - user = new ClaimsPrincipal(new ClaimsIdentity()); - NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); - } - - public void ConfirmPasswordChanged() - { - NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user ?? throw new Exception("Password cannot be changed because user was not authenticated")))); - } - - public async Task getTenantId(string jwtString) - { - JwtReader jwtReader = new JwtReader(jwtString); - int tenantId = 0; - - if (await jwtReader.Validate()) - { - ClaimsIdentity identity = new ClaimsIdentity - ( - claims: jwtReader.GetClaims(), - authenticationType: "ldap", - nameType: JwtRegisteredClaimNames.UniqueName, - roleType: "role" - ); - - // Set user information - user = new ClaimsPrincipal(identity); - - if (!int.TryParse(user.FindFirstValue("x-hasura-tenant-id"), out tenantId)) - { - // TODO: log warning - } - } - return tenantId; - } - - public async Task getTenantFromJwt(string jwtString, ApiConnection apiConnection) - { - JwtReader jwtReader = new JwtReader(jwtString); - Tenant tenant = new(); - - if (await jwtReader.Validate()) - { - ClaimsIdentity identity = new ClaimsIdentity - ( - claims: jwtReader.GetClaims(), - authenticationType: "ldap", - nameType: JwtRegisteredClaimNames.UniqueName, - roleType: "role" - ); - - // Set user information - user = new ClaimsPrincipal(identity); - - if (int.TryParse(user.FindFirstValue("x-hasura-tenant-id"), out int tenantId)) - { - tenant = await Tenant.GetSingleTenant(apiConnection, tenantId) ?? new(); - } - // else - // { - // // TODO: log warning - // } - } - return tenant; - } - - public async Task> getAllowedRoles(string jwtString) - { - return await GetClaimList(jwtString, "x-hasura-allowed-roles"); - } - - public async Task> getAssignedOwners(string jwtString) - { - List ownerIds = new(); - List ownerClaims = await GetClaimList(jwtString, "x-hasura-editable-owners"); - if(ownerClaims.Count > 0) - { - string[] separatingStrings = { ",", "{", "}" }; - string[] owners = ownerClaims[0].Split(separatingStrings, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - ownerIds = Array.ConvertAll(owners, x => int.Parse(x)).ToList(); - } - return ownerIds; - } - - private async Task> GetClaimList(string jwtString, string claimType) - { - List claimList = new List(); - JwtReader jwtReader = new JwtReader(jwtString); - if (await jwtReader.Validate()) - { - ClaimsIdentity identity = new ClaimsIdentity - ( - claims: jwtReader.GetClaims(), - authenticationType: "ldap", - nameType: JwtRegisteredClaimNames.UniqueName, - roleType: "role" - ); - foreach (Claim claim in identity.Claims) - { - if (claim.Type == claimType) - { - claimList.Add(claim.Value); - } - } - } - return claimList; - } - } + public class AuthStateProvider : AuthenticationStateProvider + { + private ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity()); + + public override Task GetAuthenticationStateAsync() + { + return Task.FromResult(new AuthenticationState(user)); + } + + public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, + GlobalConfig globalConfig, UserConfig userConfig, ProtectedSessionStorage sessionStorage, CircuitHandlerService circuitHandler) + { + // There is no jwt in session storage. Get one from auth module. + AuthenticationTokenGetParameters authenticationParameters = new AuthenticationTokenGetParameters { Username = username, Password = password }; + RestResponse apiAuthResponse = await middlewareClient.AuthenticateUser(authenticationParameters); + + if (apiAuthResponse.StatusCode == HttpStatusCode.OK) + { + string jwtString = apiAuthResponse.Data ?? throw new Exception("no response data"); + await Authenticate(jwtString, apiConnection, middlewareClient, globalConfig, userConfig, circuitHandler, sessionStorage); + Log.WriteAudit("AuthenticateUser", $"user {username} successfully authenticated"); + } + + return apiAuthResponse; + } + + public async Task Authenticate(string jwtString, ApiConnection apiConnection, MiddlewareClient middlewareClient, + GlobalConfig globalConfig, UserConfig userConfig, CircuitHandlerService circuitHandler, ProtectedSessionStorage sessionStorage) + { + // Try to auth with jwt (validates it and creates user context on UI side). + JwtReader jwtReader = new JwtReader(jwtString); + + if (await jwtReader.Validate()) + { + // importer is not allowed to login + if (jwtReader.ContainsRole(Roles.Importer)) + { + throw new AuthenticationException("login_importer_error"); + } + + // anonymous has no authorization to login via UI + if (jwtReader.ContainsRole(Roles.Anonymous)) + { + throw new AuthenticationException("not_authorized"); + } + + // Save jwt in session storage. + await sessionStorage.SetAsync("jwt", jwtString); + + // Tell api connection to use jwt as authentication + apiConnection.SetAuthHeader(jwtString); + + // Tell middleware connection to use jwt as authentication + middlewareClient.SetAuthenticationToken(jwtString); + + // Set user claims based on the jwt claims + ClaimsIdentity identity = new ClaimsIdentity + ( + claims: jwtReader.GetClaims(), + authenticationType: "ldap", + nameType: JwtRegisteredClaimNames.UniqueName, + roleType: "role" + ); + + // Set user information + user = new ClaimsPrincipal(identity); + string userDn = user.FindFirstValue("x-hasura-uuid"); + await userConfig.SetUserInformation(userDn, apiConnection); + userConfig.User.Jwt = jwtString; + userConfig.User.Tenant = await getTenantFromJwt(userConfig.User.Jwt, apiConnection); + userConfig.User.Roles = await getAllowedRoles(userConfig.User.Jwt); + userConfig.User.Ownerships = await getAssignedOwners(userConfig.User.Jwt); + circuitHandler.User = userConfig.User; + + // Add jwt expiry timer + JwtEventService.AddJwtTimers(userDn, (int)jwtReader.TimeUntilExpiry().TotalMilliseconds, 1000 * 60 * globalConfig.SessionTimeoutNoticePeriod); + + if (!userConfig.User.PasswordMustBeChanged) + { + NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); + } + } + else + { + Deauthenticate(); + } + } + + public void Deauthenticate() + { + user = new ClaimsPrincipal(new ClaimsIdentity()); + NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); + } + + public void ConfirmPasswordChanged() + { + NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user ?? throw new Exception("Password cannot be changed because user was not authenticated")))); + } + + public async Task getTenantId(string jwtString) + { + JwtReader jwtReader = new JwtReader(jwtString); + int tenantId = 0; + + if (await jwtReader.Validate()) + { + ClaimsIdentity identity = new ClaimsIdentity + ( + claims: jwtReader.GetClaims(), + authenticationType: "ldap", + nameType: JwtRegisteredClaimNames.UniqueName, + roleType: "role" + ); + + // Set user information + user = new ClaimsPrincipal(identity); + + if (!int.TryParse(user.FindFirstValue("x-hasura-tenant-id"), out tenantId)) + { + // TODO: log warning + } + } + return tenantId; + } + + public async Task getTenantFromJwt(string jwtString, ApiConnection apiConnection) + { + JwtReader jwtReader = new JwtReader(jwtString); + Tenant tenant = new(); + + if (await jwtReader.Validate()) + { + ClaimsIdentity identity = new ClaimsIdentity + ( + claims: jwtReader.GetClaims(), + authenticationType: "ldap", + nameType: JwtRegisteredClaimNames.UniqueName, + roleType: "role" + ); + + // Set user information + user = new ClaimsPrincipal(identity); + + if (int.TryParse(user.FindFirstValue("x-hasura-tenant-id"), out int tenantId)) + { + tenant = await Tenant.GetSingleTenant(apiConnection, tenantId) ?? new(); + } + // else + // { + // // TODO: log warning + // } + } + return tenant; + } + + public async Task> getAllowedRoles(string jwtString) + { + return await GetClaimList(jwtString, "x-hasura-allowed-roles"); + } + + public async Task> getAssignedOwners(string jwtString) + { + List ownerIds = new(); + List ownerClaims = await GetClaimList(jwtString, "x-hasura-editable-owners"); + if (ownerClaims.Count > 0) + { + string[] separatingStrings = { ",", "{", "}" }; + string[] owners = ownerClaims[0].Split(separatingStrings, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + ownerIds = Array.ConvertAll(owners, x => int.Parse(x)).ToList(); + } + return ownerIds; + } + + private async Task> GetClaimList(string jwtString, string claimType) + { + List claimList = new List(); + JwtReader jwtReader = new JwtReader(jwtString); + if (await jwtReader.Validate()) + { + ClaimsIdentity identity = new ClaimsIdentity + ( + claims: jwtReader.GetClaims(), + authenticationType: "ldap", + nameType: JwtRegisteredClaimNames.UniqueName, + roleType: "role" + ); + foreach (Claim claim in identity.Claims) + { + if (claim.Type == claimType) + { + claimList.Add(claim.Value); + } + } + } + return claimList; + } + } }