diff --git a/documentation/revision-history-develop.md b/documentation/revision-history-develop.md index eae4f723e..58615073d 100644 --- a/documentation/revision-history-develop.md +++ b/documentation/revision-history-develop.md @@ -97,3 +97,17 @@ In 2020 we decided to re-launch a new ### 7.1.2 - 16.08.2023 DEVELOP - adding Check Point R8x Inform action + +### 7.2 - 21.08.2023 DEVELOP +mostly version update summarizing latest PRs +- UI/API: adding tenant ip filtering beta version (clean-up and optiomazation necessary) +- API: updating hasura to 2.32.0 +- UI: now not showing super managers in RSB all tab +- UI: bug fixes blazor environment settings + - Use production / development based on the build type instead of always using development. + - Do not show detailed errors in production mode. + - Use the custom error page in the production environment. + - Spelling mistake fix +- UI: bug fix jwt expiry + - jwt expiry timer now works as intended + - after the jwt expired no exception can be triggered anymore diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index d2dd9f4f1..026f09cfd 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -1,5 +1,5 @@ ### general settings -product_version: "7.1.2" +product_version: "7.2" ansible_user: "{{ lookup('env', 'USER') }}" ansible_become_method: sudo ansible_python_interpreter: /usr/bin/python3 diff --git a/inventory/group_vars/apiserver.yml b/inventory/group_vars/apiserver.yml index d5eac5d97..70db722b8 100644 --- a/inventory/group_vars/apiserver.yml +++ b/inventory/group_vars/apiserver.yml @@ -8,7 +8,7 @@ api_hasura_admin_test_password: "not4production" api_user_email: "{{ api_user }}@{{ api_network_listening_ip_address }}" api_home: "{{ fworch_home }}/api" api_hasura_cli_bin: "{{ fworch_home }}/api/bin/hasura" -api_hasura_version: "v2.31.0" +api_hasura_version: "v2.32.0" api_project_name: api api_no_metadata: false api_rollback_is_running: false diff --git a/roles/api/files/replace_metadata.json b/roles/api/files/replace_metadata.json index 1e8474d1f..5fb3b7615 100644 --- a/roles/api/files/replace_metadata.json +++ b/roles/api/files/replace_metadata.json @@ -1627,6 +1627,18 @@ } } ], + "computed_fields": [ + { + "name": "has_relevant_change", + "definition": { + "function": { + "name": "has_relevant_change", + "schema": "public" + }, + "session_argument": "hasura_session" + } + } + ], "select_permissions": [ { "role": "auditor", @@ -1735,10 +1747,27 @@ "change_time", "unique_name" ], + "computed_fields": [ + "has_relevant_change" + ], "filter": { - "dev_id": { - "_in": "x-hasura-visible-devices" - } + "_and": [ + { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + }, + { + "dev_id": { + "_in": "x-hasura-visible-devices" + } + }, + { + "has_relevant_change": { + "_eq": "true" + } + } + ] }, "allow_aggregations": true } @@ -1767,10 +1796,27 @@ "change_time", "unique_name" ], + "computed_fields": [ + "has_relevant_change" + ], "filter": { - "dev_id": { - "_in": "x-hasura-visible-devices" - } + "_and": [ + { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + }, + { + "dev_id": { + "_in": "x-hasura-visible-devices" + } + }, + { + "has_relevant_change": { + "_eq": "true" + } + } + ] }, "allow_aggregations": true } @@ -6861,6 +6907,18 @@ } } ], + "computed_fields": [ + { + "name": "object_relevant_for_tenant", + "definition": { + "function": { + "name": "object_relevant_for_tenant", + "schema": "public" + }, + "session_argument": "hasura_session" + } + } + ], "select_permissions": [ { "role": "auditor", @@ -6976,9 +7034,18 @@ "obj_last_seen" ], "filter": { - "mgm_id": { - "_in": "x-hasura-visible-managements" - } + "_and": [ + { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + }, + { + "object_relevant_for_tenant": { + "_eq": "true" + } + } + ] }, "allow_aggregations": true } @@ -7019,9 +7086,18 @@ "obj_last_seen" ], "filter": { - "mgm_id": { - "_in": "x-hasura-visible-managements" - } + "_and": [ + { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + }, + { + "object_relevant_for_tenant": { + "_eq": "true" + } + } + ] }, "allow_aggregations": true } @@ -10027,6 +10103,18 @@ } } ], + "computed_fields": [ + { + "name": "rule_relevant_for_tenant", + "definition": { + "function": { + "name": "rule_relevant_for_tenant", + "schema": "public" + }, + "session_argument": "hasura_session" + } + } + ], "select_permissions": [ { "role": "auditor", @@ -10186,6 +10274,11 @@ "dev_id": { "_in": "x-hasura-visible-devices" } + }, + { + "rule_relevant_for_tenant": { + "_eq": "true" + } } ] }, @@ -10246,6 +10339,11 @@ "dev_id": { "_in": "x-hasura-visible-devices" } + }, + { + "rule_relevant_for_tenant": { + "_eq": "true" + } } ] }, @@ -10338,6 +10436,18 @@ } } ], + "computed_fields": [ + { + "name": "rule_from_relevant_for_tenant", + "definition": { + "function": { + "name": "rule_from_relevant_for_tenant", + "schema": "public" + }, + "session_argument": "hasura_session" + } + } + ], "select_permissions": [ { "role": "auditor", @@ -10386,7 +10496,29 @@ "active", "negated" ], - "filter": {}, + "filter": { + "_and": [ + { + "rule": { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + } + }, + { + "rule": { + "dev_id": { + "_in": "x-hasura-visible-devices" + } + } + }, + { + "rule_from_relevant_for_tenant": { + "_eq": "true" + } + } + ] + }, "allow_aggregations": true } }, @@ -10403,7 +10535,29 @@ "active", "negated" ], - "filter": {}, + "filter": { + "_and": [ + { + "rule": { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + } + }, + { + "rule": { + "dev_id": { + "_in": "x-hasura-visible-devices" + } + } + }, + { + "rule_from_relevant_for_tenant": { + "_eq": "true" + } + } + ] + }, "allow_aggregations": true } }, @@ -11144,6 +11298,18 @@ } } ], + "computed_fields": [ + { + "name": "rule_to_relevant_for_tenant", + "definition": { + "function": { + "name": "rule_to_relevant_for_tenant", + "schema": "public" + }, + "session_argument": "hasura_session" + } + } + ], "select_permissions": [ { "role": "auditor", @@ -11190,7 +11356,29 @@ "rule_to_id", "user_id" ], - "filter": {}, + "filter": { + "_and": [ + { + "rule": { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + } + }, + { + "rule": { + "dev_id": { + "_in": "x-hasura-visible-devices" + } + } + }, + { + "rule_to_relevant_for_tenant": { + "_eq": "true" + } + } + ] + }, "allow_aggregations": true } }, @@ -11207,7 +11395,29 @@ "rule_to_id", "user_id" ], - "filter": {}, + "filter": { + "_and": [ + { + "rule": { + "mgm_id": { + "_in": "x-hasura-visible-managements" + } + } + }, + { + "rule": { + "dev_id": { + "_in": "x-hasura-visible-devices" + } + } + }, + { + "rule_to_relevant_for_tenant": { + "_eq": "true" + } + } + ] + }, "allow_aggregations": true } }, diff --git a/roles/database/files/sql/idempotent/fworch-api-funcs.sql b/roles/database/files/sql/idempotent/fworch-api-funcs.sql index 5f6de591c..3afedbc2b 100644 --- a/roles/database/files/sql/idempotent/fworch-api-funcs.sql +++ b/roles/database/files/sql/idempotent/fworch-api-funcs.sql @@ -94,4 +94,272 @@ AS $function$ WHERE r.mgm_id = management_row.mgm_id AND rule_id = any (rule_ids) AND r.created <= import_id AND (r.removed IS NULL OR r.removed >= import_id) GROUP BY u.user_id ORDER BY MAX(user_name), u.user_id -$function$; \ No newline at end of file +$function$; + + +CREATE OR REPLACE FUNCTION has_relevant_change(cl_rule changelog_rule, hasura_session json) +RETURNS boolean AS $$ + DECLARE t_id integer; + show boolean DEFAULT false; + + BEGIN + t_id := (hasura_session ->> 'x-hasura-tenant-id')::integer; + + IF t_id IS NULL THEN + RAISE EXCEPTION 'No tenant id found in hasura session'; --> only happens when using auth via x-hasura-admin-secret (no tenant id is set) + ELSIF t_id = 1 THEN + show := true; + ELSE + IF EXISTS ( + SELECT diff.obj_id FROM ( -- set of difference between rule_from of old and new rule + SELECT obj_id FROM rule_from WHERE rule_id = cl_rule.old_rule_id EXCEPT SELECT obj_id FROM rule_from WHERE rule_id = cl_rule.new_rule_id + UNION + (SELECT obj_id FROM rule_from WHERE rule_id = cl_rule.new_rule_id EXCEPT SELECT obj_id FROM rule_from WHERE rule_id = cl_rule.old_rule_id) + ) AS diff + JOIN objgrp_flat ON (obj_id=objgrp_flat_id) + JOIN object ON (objgrp_flat_member_id=object.obj_id) + JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE tenant_id = t_id + ) THEN + show := true; + END IF; + + IF EXISTS ( + SELECT diff.obj_id FROM ( -- set of difference between rule_to of old and new rule + SELECT obj_id FROM rule_to WHERE rule_id = cl_rule.old_rule_id EXCEPT SELECT obj_id FROM rule_to WHERE rule_id = cl_rule.new_rule_id + UNION + (SELECT obj_id FROM rule_to WHERE rule_id = cl_rule.new_rule_id EXCEPT SELECT obj_id FROM rule_to WHERE rule_id = cl_rule.old_rule_id) + ) AS diff + JOIN objgrp_flat ON (obj_id=objgrp_flat_id) + JOIN object ON (objgrp_flat_member_id=object.obj_id) + JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE tenant_id = t_id + ) THEN + show := true; + END IF; + + END IF; + + RETURN show; + END; +$$ LANGUAGE 'plpgsql' STABLE; + + +CREATE OR REPLACE FUNCTION rule_relevant_for_tenant(rule rule, hasura_session json) +RETURNS boolean AS $$ + DECLARE t_id integer; + show boolean DEFAULT false; + + BEGIN + t_id := (hasura_session ->> 'x-hasura-tenant-id')::integer; + + IF t_id IS NULL THEN + RAISE EXCEPTION 'No tenant id found in hasura session'; --> only happens when using auth via x-hasura-admin-secret (no tenant id is set) + ELSIF t_id = 1 THEN + show := true; + ELSE + IF EXISTS ( + SELECT rule_from.obj_id FROM rule_from + LEFT JOIN objgrp_flat ON (rule_from.obj_id=objgrp_flat.objgrp_flat_id) + LEFT JOIN object ON (objgrp_flat.objgrp_flat_member_id=object.obj_id) + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE rule_from.rule_id = rule.rule_id AND tenant_id = t_id + ) THEN + show := true; + END IF; + + IF EXISTS ( + SELECT rule_to.obj_id FROM rule_to + LEFT JOIN objgrp_flat ON (rule_to.obj_id=objgrp_flat.objgrp_flat_id) + LEFT JOIN object ON (objgrp_flat.objgrp_flat_member_id=object.obj_id) + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE rule_to.rule_id = rule.rule_id AND tenant_id = t_id + ) THEN + show := true; + END IF; + + END IF; + + RETURN show; + END; +$$ LANGUAGE 'plpgsql' STABLE; + + + +CREATE OR REPLACE FUNCTION rule_from_relevant_for_tenant(rule_from rule_from, hasura_session json) +RETURNS boolean AS $$ + DECLARE t_id integer; + show boolean DEFAULT false; + rule_to_obj RECORD; + + BEGIN + t_id := (hasura_session ->> 'x-hasura-tenant-id')::integer; + + IF t_id IS NULL THEN + RAISE EXCEPTION 'No tenant id found in hasura session'; --> only happens when using auth via x-hasura-admin-secret (no tenant id is set) + ELSIF t_id = 1 THEN + show := true; + ELSE + IF EXISTS ( -- ip of rule_from object is in tenant_network of tenant + SELECT rf.obj_id FROM rule_from rf + LEFT JOIN objgrp_flat ON (rf.obj_id=objgrp_flat.objgrp_flat_id) + LEFT JOIN object ON (objgrp_flat.objgrp_flat_member_id=object.obj_id) + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE rule_from_id = rule_from.rule_from_id AND tenant_id = t_id --> this better be efficient (rule_from_id checked before join) (!TODO: check this) + ) THEN + show := true; + ELSE -- check if all rule_from objects visible since relevant rule_to exists + FOR rule_to_obj IN + SELECT rt.*, tenant_network.tenant_id + FROM rule_to rt + LEFT JOIN objgrp_flat ON (rt.obj_id=objgrp_flat_id) + LEFT JOIN object ON (objgrp_flat_member_id=object.obj_id) + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE rule_id = rule_from.rule_id + LOOP + IF rule_to_obj.tenant_id = t_id THEN + show := true; + EXIT; + END IF; + END LOOP; + END IF; + + END IF; + + RETURN show; + END; +$$ LANGUAGE 'plpgsql' STABLE; + + +CREATE OR REPLACE FUNCTION rule_to_relevant_for_tenant(rule_to rule_to, hasura_session json) +RETURNS boolean AS $$ + DECLARE t_id integer; + show boolean DEFAULT false; + rule_from_obj RECORD; + + BEGIN + t_id := (hasura_session ->> 'x-hasura-tenant-id')::integer; + + IF t_id IS NULL THEN + RAISE EXCEPTION 'No tenant id found in hasura session'; --> only happens when using auth via x-hasura-admin-secret (no tenant id is set) + ELSIF t_id = 1 THEN + show := true; + ELSE + IF EXISTS ( -- ip of rule_to object is in tenant_network of tenant + SELECT rt.obj_id FROM rule_to rt + LEFT JOIN objgrp_flat ON (rt.obj_id=objgrp_flat.objgrp_flat_id) + LEFT JOIN object ON (objgrp_flat.objgrp_flat_member_id=object.obj_id) + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE rule_to_id = rule_to.rule_to_id AND tenant_id = t_id --> this better be efficient (rule_to_id checked before join) (!TODO: check this) + ) THEN + show := true; + ELSE -- check if all rule_to objects visible since relevant rule_from exists + FOR rule_from_obj IN + SELECT rf.*, tenant_network.tenant_id + FROM rule_from rf + LEFT JOIN objgrp_flat ON (rf.obj_id=objgrp_flat_id) + LEFT JOIN object ON (objgrp_flat.objgrp_flat_member_id=object.obj_id) + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE rule_id = rule_to.rule_id + LOOP + IF rule_from_obj.tenant_id = t_id THEN + show := true; + EXIT; + END IF; + END LOOP; + END IF; + + END IF; + + RETURN show; + END; +$$ LANGUAGE 'plpgsql' STABLE; + + +CREATE OR REPLACE FUNCTION object_relevant_for_tenant(object object, hasura_session json) -- todo: try over all objects in rule_from and rule_to +RETURNS boolean AS $$ + DECLARE t_id integer; + show boolean DEFAULT false; + + BEGIN + t_id := (hasura_session ->> 'x-hasura-tenant-id')::integer; + + IF t_id IS NULL THEN + RAISE EXCEPTION 'No tenant id found in hasura session'; --> only happens when using auth via x-hasura-admin-secret (no tenant id is set) + ELSIF t_id = 1 THEN + show := true; + ELSIF EXISTS ( -- ip of object is in tenant_network of tenant + SELECT o.obj_id FROM object o + LEFT JOIN tenant_network ON + (obj_ip>>=tenant_net_ip OR obj_ip<<=tenant_net_ip) + WHERE obj_id = object.obj_id AND tenant_id = t_id + ) THEN + show := true; + ELSIF EXISTS ( -- object is in rule_from or rule_to of a rule that is visible to tenant + SELECT r.rule_id from rule r + LEFT JOIN rule_from ON (r.rule_id=rule_from.rule_id) + LEFT JOIN rule_to ON (r.rule_id=rule_to.rule_id) + LEFT JOIN objgrp_flat rf_of ON (rule_from.obj_id=rf_of.objgrp_flat_id) + LEFT JOIN objgrp_flat rt_of ON (rule_to.obj_id=rt_of.objgrp_flat_id) + LEFT JOIN object rf_o ON (rf_of.objgrp_flat_member_id=rf_o.obj_id) + LEFT JOIN object rt_o ON (rt_of.objgrp_flat_member_id=rt_o.obj_id) + LEFT JOIN tenant_network ON + (rf_o.obj_ip>>=tenant_net_ip OR rf_o.obj_ip<<=tenant_net_ip OR rt_o.obj_ip>>=tenant_net_ip OR rt_o.obj_ip<<=tenant_net_ip) + WHERE (rf_o.obj_id = object.obj_id OR rt_o.obj_id = object.obj_id) AND tenant_id = t_id + ) THEN + show := true; + END IF; + + RETURN show; + END; +$$ LANGUAGE 'plpgsql' STABLE; + + +CREATE OR REPLACE FUNCTION get_objects_for_tenant(management_row management, tenant integer, hasura_session json) +RETURNS SETOF object AS $$ + DECLARE t_id integer; + + BEGIN + t_id := (hasura_session ->> 'x-hasura-tenant-id')::integer; + + IF t_id IS NULL THEN + RAISE EXCEPTION 'No tenant id found in hasura session'; --> only happens when using auth via x-hasura-admin-secret (no tenant id is set) + ELSIF t_id != 1 THEN + RAISE EXCEPTION 'Tenant id in hasura session is not 1 (admin). Tenant simulation not allowed.'; + ELSIF tenant = 1 THEN + RAISE EXCEPTION 'Tenant 1 (admin) cannot be simulated.'; + ELSE + RETURN QUERY + SELECT o.* FROM ( + SELECT o.* FROM object o + LEFT JOIN rule_from ON (o.obj_id=rule_from.obj_id) + LEFT JOIN rule r ON (rule_from.rule_id=r.rule_id) + LEFT JOIN rule_to ON (r.rule_id=rule_to.rule_id) + LEFT JOIN objgrp_flat rt_of ON (rule_to.obj_id=rt_of.objgrp_flat_id) + LEFT JOIN object rt_o ON (rt_of.objgrp_flat_member_id=rt_o.obj_id) + LEFT JOIN tenant_network ON + (o.obj_ip>>=tenant_net_ip OR o.obj_ip<<=tenant_net_ip OR rt_o.obj_ip>>=tenant_net_ip OR rt_o.obj_ip<<=tenant_net_ip) + WHERE r.mgm_id = management_row.mgm_id AND tenant_id = tenant + UNION + SELECT o.* FROM object o + LEFT JOIN rule_to ON (o.obj_id=rule_to.obj_id) + LEFT JOIN rule r ON (rule_to.rule_id=r.rule_id) + LEFT JOIN rule_from ON (r.rule_id=rule_from.rule_id) + LEFT JOIN objgrp_flat rf_of ON (rule_from.obj_id=rf_of.objgrp_flat_id) + LEFT JOIN object rf_o ON (rf_of.objgrp_flat_member_id=rf_o.obj_id) + LEFT JOIN tenant_network ON + (o.obj_ip>>=tenant_net_ip OR o.obj_ip<<=tenant_net_ip OR rf_o.obj_ip>>=tenant_net_ip OR rf_o.obj_ip<<=tenant_net_ip) + WHERE r.mgm_id = management_row.mgm_id AND tenant_id = tenant + ) AS o + ORDER BY obj_name; + END IF; + END; +$$ LANGUAGE 'plpgsql' STABLE; diff --git a/roles/importer/importer.pyproj b/roles/importer/importer.pyproj deleted file mode 100644 index 6beb77572..000000000 --- a/roles/importer/importer.pyproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - Debug - 2.0 - {1a1b90a0-227d-4041-a62a-f83af9c9c7cf} - - files\importer\import-mgm.py - - . - . - {888888a0-9f3d-457c-b088-3a5042f75d52} - Standard Python launcher - - -m4 -d4 -f -iC:/Users/Nils/Downloads/fortiManager_NAT_mgm_id_25_config_native.json.anon -l250 - False - - - - - 10.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/roles/lib/files/FWO.Api.Client/APIcalls/allObjects/getAllObjectDetails.graphql b/roles/lib/files/FWO.Api.Client/APIcalls/allObjects/getAllObjectDetails.graphql index e62d3c8f9..d518ce332 100644 --- a/roles/lib/files/FWO.Api.Client/APIcalls/allObjects/getAllObjectDetails.graphql +++ b/roles/lib/files/FWO.Api.Client/APIcalls/allObjects/getAllObjectDetails.graphql @@ -12,6 +12,7 @@ query getAllObjectDetails ( hide_in_gui: { _eq: false } mgm_id: { _in: $management_id } stm_dev_typ:{ + dev_typ_is_multi_mgmt: { _eq: false } is_pure_routing_device:{_eq:false} } } diff --git a/roles/lib/files/FWO.Api.Client/APIcalls/networkObject/getTenantNetworkObjectDetails.graphql b/roles/lib/files/FWO.Api.Client/APIcalls/networkObject/getTenantNetworkObjectDetails.graphql new file mode 100644 index 000000000..935149b21 --- /dev/null +++ b/roles/lib/files/FWO.Api.Client/APIcalls/networkObject/getTenantNetworkObjectDetails.graphql @@ -0,0 +1,29 @@ +query getNetworkObjectDetails( + $management_id: [Int!] + $nwObjTyp: [String!] + $nwObjUid: [String!] + $time: String + $obj_name: [String!] + $obj_ip: [cidr!] + $limit: Int + $offset: Int +) { + management(where: { mgm_id: { _in: $management_id }, stm_dev_typ:{dev_typ_is_multi_mgmt:{_eq:false}} }) { + id: mgm_id + name: mgm_name + networkObjects: get_objects_for_tenant( + limit: $limit + offset: $offset + where: { + stm_obj_typ: { obj_typ_name: { _in: $nwObjTyp } } + active: { _eq: true } + obj_name: { _in: $obj_name } + obj_ip: { _in: $obj_ip } + obj_uid: { _in: $nwObjUid } + } + order_by: { obj_name: asc } + ) { + ...networkObjectDetails + } + } +} diff --git a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs index 95edf0aeb..dd73a8749 100644 --- a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs +++ b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs @@ -26,7 +26,7 @@ public override Task GetAuthenticationStateAsync() } public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, - UserConfig userConfig, ProtectedSessionStorage sessionStorage, CircuitHandlerService circuitHandler) + 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 }; @@ -35,7 +35,7 @@ public async Task> Authenticate(string username, string pas if (apiAuthResponse.StatusCode == HttpStatusCode.OK) { string jwtString = apiAuthResponse.Data ?? throw new Exception("no response data"); - await Authenticate(jwtString, apiConnection, middlewareClient, userConfig, circuitHandler, sessionStorage); + await Authenticate(jwtString, apiConnection, middlewareClient, globalConfig, userConfig, circuitHandler, sessionStorage); Log.WriteAudit("AuthenticateUser", $"user {username} successfully authenticated"); } @@ -43,7 +43,7 @@ public async Task> Authenticate(string username, string pas } public async Task Authenticate(string jwtString, ApiConnection apiConnection, MiddlewareClient middlewareClient, - UserConfig userConfig, CircuitHandlerService circuitHandler, ProtectedSessionStorage sessionStorage) + 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); @@ -65,9 +65,6 @@ public async Task Authenticate(string jwtString, ApiConnection apiConnection, Mi // Tell middleware connection to use jwt as authentication middlewareClient.SetAuthenticationToken(jwtString); - // Add jwt expiry timer - JwtEventService.AddJwtTimers(userConfig.User.Dn, (int)jwtReader.TimeUntilExpiry().TotalMilliseconds, 1000 * 60 * userConfig.SessionTimeoutNoticePeriod); - // Set user claims based on the jwt claims ClaimsIdentity identity = new ClaimsIdentity ( @@ -77,11 +74,15 @@ public async Task Authenticate(string jwtString, ApiConnection apiConnection, Mi roleType: "role" ); + // Set user information user = new ClaimsPrincipal(identity); - - await userConfig.SetUserInformation(user.FindFirstValue("x-hasura-uuid"), apiConnection); - circuitHandler.User = userConfig.User; + string userDn = user.FindFirstValue("x-hasura-uuid"); + await userConfig.SetUserInformation(userDn, apiConnection); userConfig.User.Jwt = jwtString; + circuitHandler.User = userConfig.User; + + // Add jwt expiry timer + JwtEventService.AddJwtTimers(userDn, (int)jwtReader.TimeUntilExpiry().TotalMilliseconds, 1000 * 60 * globalConfig.SessionTimeoutNoticePeriod); if (!userConfig.User.PasswordMustBeChanged) { diff --git a/roles/ui/files/FWO.UI/Pages/Login.razor b/roles/ui/files/FWO.UI/Pages/Login.razor index b623228b6..31dde3b44 100644 --- a/roles/ui/files/FWO.UI/Pages/Login.razor +++ b/roles/ui/files/FWO.UI/Pages/Login.razor @@ -121,7 +121,7 @@ try { await ((AuthStateProvider)AuthService).Authenticate(jwtLoadRequest.Value ?? throw new AccessViolationException("Jwt from protected storage is null"), - ApiConnection, middlewareClient, userConfig, ((CircuitHandlerService)circuitHandler), sessionStorage); + ApiConnection, middlewareClient, globalConfig, userConfig, ((CircuitHandlerService)circuitHandler), sessionStorage); return; } catch (Exception ex) { Log.WriteError("Session Restore", "Session restore unsuccessful.", ex); } @@ -174,7 +174,7 @@ try { RestResponse authResponse = await ((AuthStateProvider)AuthService) - .Authenticate(Username, Password, ApiConnection, middlewareClient, userConfig, sessionStorage, ((CircuitHandlerService)circuitHandler)); + .Authenticate(Username, Password, ApiConnection, middlewareClient, globalConfig, userConfig, sessionStorage, ((CircuitHandlerService)circuitHandler)); if (authResponse.StatusCode == HttpStatusCode.OK) { diff --git a/roles/ui/files/FWO.UI/Pages/_Host.cshtml b/roles/ui/files/FWO.UI/Pages/_Host.cshtml index 3ae4071a1..bf0e19427 100644 --- a/roles/ui/files/FWO.UI/Pages/_Host.cshtml +++ b/roles/ui/files/FWO.UI/Pages/_Host.cshtml @@ -37,10 +37,6 @@ - + diff --git a/roles/ui/files/FWO.UI/Program.cs b/roles/ui/files/FWO.UI/Program.cs index 9be2d36c7..5def98a55 100644 --- a/roles/ui/files/FWO.UI/Program.cs +++ b/roles/ui/files/FWO.UI/Program.cs @@ -1,35 +1,103 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; +using BlazorTable; +using FWO.Api.Client; +using FWO.Config.Api; +using FWO.Config.File; using FWO.Logging; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using FWO.Middleware.Client; +using FWO.Ui.Auth; +using FWO.Ui.Services; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server.Circuits; +using RestSharp; +using System.Diagnostics; +using FWO.Ui; -namespace FWO.Ui +// Implicitly call static constructor so backround lock process is started +// (static constructor is only called after class is used in any way) +Log.WriteInfo("Startup", "Starting FWO UI Server..."); + +var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseStaticWebAssets(); + +/// Add services to the container. +#region Services + +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +string ApiUri = ConfigFile.ApiServerUri; +string MiddlewareUri = ConfigFile.MiddlewareServerUri; +string ProductVersion = ConfigFile.ProductVersion; + +builder.Services.AddScoped(_ => new GraphQlApiConnection(ApiUri)); +builder.Services.AddScoped(_ => new MiddlewareClient(MiddlewareUri)); + +// Create "anonymous" (empty) jwt +MiddlewareClient middlewareClient = new MiddlewareClient(MiddlewareUri); +ApiConnection apiConn = new GraphQlApiConnection(ApiUri); + +RestResponse createJWTResponse = middlewareClient.CreateInitialJWT().Result; +bool connectionEstablished = createJWTResponse.IsSuccessful; +int connectionAttemptsCount = 1; +while (!connectionEstablished) { - public class Program - { - public static void Main(string[] args) - { - // Implicitly call static constructor so backround lock process is started - // (static constructor is only called after class is used in any way) - Log.WriteInfo("Startup", "Starting FWO UI Server..."); - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStaticWebAssets(); - webBuilder.UseStartup(); - }); - } - } + Log.WriteError("Middleware Server Connection", + $"Error while authenticating as anonymous user from UI (Attempt {connectionAttemptsCount}), " + + $"Uri: {createJWTResponse.ResponseUri?.AbsoluteUri}, " + + $"HttpStatus: {createJWTResponse.StatusDescription}, " + + $"Error: {createJWTResponse.ErrorMessage}"); + Thread.Sleep(500 * connectionAttemptsCount++); + createJWTResponse = middlewareClient.CreateInitialJWT().Result; + connectionEstablished = createJWTResponse.IsSuccessful; } + +string jwt = createJWTResponse.Data ?? throw new NullReferenceException("Received empty jwt."); +apiConn.SetAuthHeader(jwt); + +// Get all non-confidential configuration settings and add to a global service (for all users) +GlobalConfig globalConfig = Task.Run(async () => await GlobalConfig.ConstructAsync(jwt)).Result; +builder.Services.AddSingleton(_ => globalConfig); +builder.Services.AddScoped(_ => new UserConfig(globalConfig)); + +builder.Services.AddScoped(_ => new NetworkZoneService()); +builder.Services.AddScoped(_ => new DomEventService()); + +builder.Services.AddBlazorTable(); + +#endregion + +var app = builder.Build(); + +//// Configure the HTTP request pipeline. +#region HTTP Request Pipeline + +Log.WriteInfo("Environment", $"{app.Environment.ApplicationName} runs in {app.Environment.EnvironmentName} Mode."); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + // app.UseHsts(); +} + +// app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +#endregion + +app.Run(); diff --git a/roles/ui/files/FWO.UI/Properties/launchSettings.json b/roles/ui/files/FWO.UI/Properties/launchSettings.json index 6eb910591..688f8495b 100644 --- a/roles/ui/files/FWO.UI/Properties/launchSettings.json +++ b/roles/ui/files/FWO.UI/Properties/launchSettings.json @@ -5,7 +5,7 @@ "launchBrowser": true, "applicationUrl": "http://localhost:5000", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Developement" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/roles/ui/files/FWO.UI/Services/JwtEventService.cs b/roles/ui/files/FWO.UI/Services/JwtEventService.cs index fce4fd322..7b9451f33 100644 --- a/roles/ui/files/FWO.UI/Services/JwtEventService.cs +++ b/roles/ui/files/FWO.UI/Services/JwtEventService.cs @@ -42,7 +42,7 @@ public static void AddJwtTimers(string userDn, int timeUntilyExpiry, int notific jwtExpiredTimers[userDn].Dispose(); } // Create new timers - if (timeUntilyExpiry - notificationTime > 0) + if (notificationTime > 0 && timeUntilyExpiry - notificationTime > 0) { jwtAboutToExpireTimers[userDn] = new Timer(_ => JwtAboutToExpire(userDn), null, timeUntilyExpiry - notificationTime, int.MaxValue); } diff --git a/roles/ui/files/FWO.UI/Shared/MainLayout.razor b/roles/ui/files/FWO.UI/Shared/MainLayout.razor index eca513d4d..bb2dd01db 100644 --- a/roles/ui/files/FWO.UI/Shared/MainLayout.razor +++ b/roles/ui/files/FWO.UI/Shared/MainLayout.razor @@ -13,6 +13,7 @@ @using RestSharp @inject ApiConnection apiConnection +@inject GlobalConfig globalConfig @inject UserConfig userConfig @inject MiddlewareClient middlewareClient @inject NavigationManager NavigationManager @@ -39,21 +40,37 @@
@dialogTitle - @dialogMessage
- @if (showReloginInput) - { -
- -
- -
+
+} + + + +

@(reloginDialogText)

+
+
+
- @if (!string.IsNullOrEmpty(reloginErrorMessage)) + +
+ @if (!string.IsNullOrEmpty(reloginErrorMessage)) + { + + } + +
+
+ @if (reloginAbortable) { - + } - } -
-} + else + { + @(userConfig.GetText("logout")) + } + + +
+
@code { @@ -72,7 +89,10 @@ private string reloginErrorMessage = ""; private string reloginCssClass = ""; - private bool showReloginInput; + private string reloginDialogTitle = ""; + private string reloginDialogText = ""; + private bool showReloginDialog; + private bool reloginAbortable; private string? password; private ClaimsPrincipal? user; @@ -109,8 +129,7 @@ { if (userDn == userConfig.User.Dn) { - showReloginInput = true; - ShowMessage(userConfig.GetText("permissions_title"), userConfig.GetText("permissions_text"), MessageType.Info, showtime: -1); + ShowReloginDialog(userConfig.GetText("permissions_title"), userConfig.GetText("permissions_text"), reloginAbortable: true); } } @@ -118,8 +137,7 @@ { if (userDn == userConfig.User.Dn) { - showReloginInput = true; - ShowMessage(userConfig.GetText("jwt_expiry_title"), userConfig.GetText("jwt_expiry_text"), MessageType.Info, showtime: -1); + ShowReloginDialog(userConfig.GetText("jwt_expiry_title"), userConfig.GetText("jwt_expiry_text"), reloginAbortable: true); } } @@ -127,11 +145,21 @@ { if (userDn == userConfig.User.Dn) { - showReloginInput = true; - ShowMessage(userConfig.GetText("jwt_expired_title"), userConfig.GetText("jwt_expired_text"), MessageType.Info, showtime: -1); + ShowReloginDialog(userConfig.GetText("jwt_expired_title"), userConfig.GetText("jwt_expired_text"), reloginAbortable: false); } } + private void ShowReloginDialog(string title, string text, bool reloginAbortable) + { + reloginErrorMessage = ""; + password = ""; + this.reloginAbortable = reloginAbortable; + reloginDialogTitle = title; + reloginDialogText = text; + showReloginDialog = true; + InvokeAsync(StateHasChanged); + } + private async Task Relogin() { string errorMessage = userConfig.GetText("relogin_error"); @@ -142,11 +170,10 @@ try { RestResponse authResponse = await ((AuthStateProvider)authenticationProvider).Authenticate(userConfig.User.Name, - password, apiConnection, middlewareClient, userConfig, sessionStorage, ((CircuitHandlerService)circuitHandler)); + password, apiConnection, middlewareClient, globalConfig, userConfig, sessionStorage, ((CircuitHandlerService)circuitHandler)); if (authResponse.StatusCode == System.Net.HttpStatusCode.OK) { - showReloginInput = false; - messageDialogShow = false; + showReloginDialog = false; reloginErrorMessage = ""; password = ""; return; @@ -154,7 +181,7 @@ else { // There was an error trying to authenticate the user. Probably invalid credentials - errorMessage = (authResponse.Data != null ? userConfig.GetApiText(authResponse.Data) : "Middleware Api Error: " + authResponse.Content); + errorMessage = (authResponse.Data != null ? userConfig.GetApiText(authResponse.Data) : "Middleware Api Error: " + authResponse.Content); } } catch (Exception ex) @@ -256,9 +283,9 @@ { Log.WriteError("Auth Token Error", "JWT expired in session.", exception); // TODO: Improve error handling for jwt expiry / api unreachable, so that no action leads to unhandled exception - if (!showReloginInput) + if (!showReloginDialog) { - showReloginInput = true; + showReloginDialog = true; ShowMessage(userConfig.GetText("jwt_expired_title"), userConfig.GetText("jwt_expired_text"), MessageType.Info, showtime: -1); } return; diff --git a/roles/ui/files/FWO.UI/Startup.cs b/roles/ui/files/FWO.UI/Startup.cs deleted file mode 100644 index fc702151a..000000000 --- a/roles/ui/files/FWO.UI/Startup.cs +++ /dev/null @@ -1,103 +0,0 @@ -using FWO.Api.Client; -using FWO.Ui.Auth; -using FWO.Middleware.Client; -using Microsoft.AspNetCore.Components.Authorization; -using FWO.Config.File; -using FWO.Config.Api; -using FWO.Logging; -using FWO.Ui.Services; -using BlazorTable; -using RestSharp; -using Microsoft.AspNetCore.Components.Server.Circuits; - -namespace FWO.Ui -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddRazorPages(); - services.AddServerSideBlazor(); - - services.AddScoped(); - services.AddScoped(); - - string ApiUri = ConfigFile.ApiServerUri; - string MiddlewareUri = ConfigFile.MiddlewareServerUri; - string ProductVersion = ConfigFile.ProductVersion; - - services.AddScoped(_ => new GraphQlApiConnection(ApiUri)); - services.AddScoped(_ => new MiddlewareClient(MiddlewareUri)); - - // create "anonymous" (empty) jwt - MiddlewareClient middlewareClient = new MiddlewareClient(MiddlewareUri); - ApiConnection apiConn = new GraphQlApiConnection(ApiUri); - - RestResponse createJWTResponse = middlewareClient.CreateInitialJWT().Result; - bool connectionEstablished = createJWTResponse.IsSuccessful; - int connectionAttemptsCount = 1; - while (!connectionEstablished) - { - Log.WriteError("Middleware Server Connection", - $"Error while authenticating as anonymous user from UI (Attempt {connectionAttemptsCount}), " - + $"Uri: {createJWTResponse.ResponseUri?.AbsoluteUri}, " - + $"HttpStatus: {createJWTResponse.StatusDescription}, " - + $"Error: {createJWTResponse.ErrorMessage}"); - Thread.Sleep(500 * connectionAttemptsCount++); - createJWTResponse = middlewareClient.CreateInitialJWT().Result; - connectionEstablished = createJWTResponse.IsSuccessful; - } - - string jwt = createJWTResponse.Data ?? throw new NullReferenceException("Received empty jwt."); - apiConn.SetAuthHeader(jwt); - - // get all non-confidential configuration settings and add to a global service (for all users) - GlobalConfig globalConfig = Task.Run(async () => await GlobalConfig.ConstructAsync(jwt)).Result; - services.AddSingleton(_ => globalConfig); - services.AddScoped(_ => new UserConfig(globalConfig)); - - services.AddScoped(_ => new NetworkZoneService()); - services.AddScoped(_ => new DomEventService()); - - services.AddBlazorTable(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - // app.UseHsts(); - } - - // app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); - }); - } - } -} diff --git a/roles/ui/files/FWO.UI/appsettings.json b/roles/ui/files/FWO.UI/appsettings.json index fe79527be..d9d9a9bff 100644 --- a/roles/ui/files/FWO.UI/appsettings.json +++ b/roles/ui/files/FWO.UI/appsettings.json @@ -1,5 +1,4 @@ { - "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information",