Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into dev
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
# Conflicts: # fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
This commit is contained in:
+9
@@ -172,6 +172,15 @@ public class SecurityConfig {
|
|||||||
// IP allowlist) — a request with no key and no Fineract login still gets nothing,
|
// IP allowlist) — a request with no key and no Fineract login still gets nothing,
|
||||||
// it just fails inside the handler instead of at this filter.
|
// it just fails inside the handler instead of at this filter.
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clientaccounts/**")).permitAll()
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clientaccounts/**")).permitAll()
|
||||||
|
// Tenant menu access — same dual-mode X-Api-Key posture as clientaccounts above
|
||||||
|
// for the two GETs (TenantMenuAccessApiResource#read still requires an ordinary
|
||||||
|
// session when no key is sent, same as ClientAllAccountsApiResource does; permitAll
|
||||||
|
// here only opens Spring Security's outer gate for the key path, it does not skip
|
||||||
|
// the handler's own check). The PUT has no session fallback inside the handler at
|
||||||
|
// all, even for a logged-in super user — see TenantMenuAccessApiResource#save.
|
||||||
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/tenant-menu-access")).permitAll()
|
||||||
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/tenant-menu-access/tenants")).permitAll()
|
||||||
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/tenant-menu-access")).permitAll()
|
||||||
// Login OTP — same posture as PAN/GSTIN public verification: provider-credentialed,
|
// Login OTP — same posture as PAN/GSTIN public verification: provider-credentialed,
|
||||||
// no Fineract auth required.
|
// no Fineract auth required.
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/login/otp/generate")).permitAll()
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/login/otp/generate")).permitAll()
|
||||||
|
|||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.security.apiclient;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.HeaderParam;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.WebApplicationException;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which top-nav menu sections a tenant is allowed to ever show, regardless of what any role inside
|
||||||
|
* that tenant grants itself.
|
||||||
|
*
|
||||||
|
* {@code HasPermissionDirective} on the frontend already hides a menu item a user's own role lacks --
|
||||||
|
* but a role carrying {@code ALL_FUNCTIONS} (a super user, or a role simply misconfigured to hold it,
|
||||||
|
* as the "Clerk" role was found to in two tenants this session) bypasses that check unconditionally.
|
||||||
|
* A bank's own admin screen for roles and permissions therefore cannot be trusted as the only gate on
|
||||||
|
* "does this bank ever get to see the HRM/NPA/Admin section" -- whoever ends up with ALL_FUNCTIONS in
|
||||||
|
* that tenant, today or after some future role edit, would still see it. This table is the ceiling
|
||||||
|
* that check now also has to clear.
|
||||||
|
*
|
||||||
|
* Reading it accepts either credential (see {@link #read} below) -- a bank's own user needs their own
|
||||||
|
* tenant's list to render their nav correctly, the same way they already read their own permissions,
|
||||||
|
* and the platform operator needs to read ANY tenant's current list before toggling it. Writing it
|
||||||
|
* accepts only the platform's own {@code X-Api-Key} + {@link ApiClientCredentialService} credential --
|
||||||
|
* the same mechanism {@code ClientAllAccountsApiResource} uses for third-party callers, but with no
|
||||||
|
* session fallback at all here. That is deliberate -- a bank's own super user must never be able to
|
||||||
|
* lift its own ceiling by granting themselves a permission from inside their own tenant, which is
|
||||||
|
* exactly what a normal {@code ALL_FUNCTIONS} gate would allow. The key is minted once via
|
||||||
|
* {@code POST /v1/apiclients} by the platform operator and never handed to a bank.
|
||||||
|
*/
|
||||||
|
@Path("/v1/tenant-menu-access")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Tenant Menu Access",
|
||||||
|
description = "Per-tenant cap on which top-nav menu sections can ever be shown, independent of any role's own permissions. Read by any logged-in user of the tenant; written only via a platform-operator API key.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TenantMenuAccessApiResource {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final ApiClientCredentialService apiClientCredentialService;
|
||||||
|
private final TenantDetailsService tenantDetailsService;
|
||||||
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
|
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP" };
|
||||||
|
|
||||||
|
// The whole, fixed set of top-nav menu codes this screen knows about -- see 0541 for how this
|
||||||
|
// list was seeded. A write is only ever a PATCH over this table's existing rows (see save()); a
|
||||||
|
// code outside this set cannot be created by the API, so a typo in a request body is silently a
|
||||||
|
// no-op rather than junk data the frontend would then have to guard against.
|
||||||
|
private static final Set<String> KNOWN_MENU_CODES = Set.of("MENU_INSTITUTION", "SIDEBAR_CFCFRMS", "MENU_TASKS",
|
||||||
|
"MENU_CASHIER", "MENU_CLERK", "MENU_BRANCH_OPS", "MENU_CLEARING", "MENU_DEPOSITS", "MENU_ADVANCES",
|
||||||
|
"MENU_LOAN_ORIGINATION", "MENU_SHARE", "MENU_NPA", "MENU_GENERAL_LEDGER", "MENU_SUB_LEDGER", "MENU_ACCOUNTING",
|
||||||
|
"MENU_CKYC_MGMT", "MENU_INSURANCE", "MENU_DIGITAL", "MENU_HRM", "MENU_ADMIN", "MENU_RBI", "MENU_INVESTMENT",
|
||||||
|
"MENU_REPORTS", "MENU_OTHERS", "MENU_CONFIG_WIZARD", "MENU_CHEQUEBOOK", "MENU_LETTER_MGMT");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This tenant's own menu-access list, for the tenant named in {@code Fineract-Platform-TenantId}.
|
||||||
|
* Dual-mode read, same posture as {@code ClientAllAccountsApiResource}: an ordinary logged-in
|
||||||
|
* user reads their own tenant's list exactly like they already read their own permission list
|
||||||
|
* (no elevated permission required), or the platform operator reads ANY tenant's list with the
|
||||||
|
* API key, to know the current state before toggling it on their own control screen. Only the
|
||||||
|
* write side ({@link #save}) has no session fallback -- reading configuration is not sensitive,
|
||||||
|
* changing it is.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String read(@HeaderParam("X-Api-Key") final String apiKey, @Context final HttpServletRequest servletRequest) {
|
||||||
|
if (StringUtils.isNotBlank(apiKey)) {
|
||||||
|
apiClientCredentialService.authenticate(apiKey, resolveClientIp(servletRequest));
|
||||||
|
} else {
|
||||||
|
context.authenticatedUser();
|
||||||
|
}
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT menu_code, enabled FROM m_tenant_menu_access");
|
||||||
|
final JsonArray out = new JsonArray();
|
||||||
|
for (final Map<String, Object> row : rows) {
|
||||||
|
final JsonObject o = new JsonObject();
|
||||||
|
o.addProperty("menuCode", (String) row.get("menu_code"));
|
||||||
|
o.addProperty("enabled", Boolean.TRUE.equals(row.get("enabled")));
|
||||||
|
out.add(o);
|
||||||
|
}
|
||||||
|
return gson.toJson(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every tenant configured on this platform, for the operator's own tenant picker -- always the
|
||||||
|
* master list from the tenant-store database, never the one tenant the caller's own
|
||||||
|
* {@code Fineract-Platform-TenantId} header happened to resolve to. That header still has to name
|
||||||
|
* a real tenant for the request to reach this handler at all (see
|
||||||
|
* {@code TenantAwareBasicAuthenticationFilter}); which one is otherwise irrelevant here.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/tenants")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String listTenants(@HeaderParam("X-Api-Key") final String apiKey, @Context final HttpServletRequest servletRequest) {
|
||||||
|
requireApiKey(apiKey, servletRequest);
|
||||||
|
final JsonArray out = new JsonArray();
|
||||||
|
for (final FineractPlatformTenant tenant : tenantDetailsService.findAllTenants()) {
|
||||||
|
final JsonObject o = new JsonObject();
|
||||||
|
o.addProperty("identifier", tenant.getTenantIdentifier());
|
||||||
|
o.addProperty("name", tenant.getName());
|
||||||
|
out.add(o);
|
||||||
|
}
|
||||||
|
return gson.toJson(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets which menu codes the tenant named in {@code Fineract-Platform-TenantId} may show, from the
|
||||||
|
* operator's own control screen. API key only -- see the class comment for why a normal session,
|
||||||
|
* even a super user's, is never accepted here.
|
||||||
|
*
|
||||||
|
* Body: {@code [{"menuCode": "MENU_HRM", "enabled": false}, ...]}. Only codes already in
|
||||||
|
* {@link #KNOWN_MENU_CODES} are written; anything else in the body is ignored rather than
|
||||||
|
* rejected, so the operator's screen can always send its full known set without first checking
|
||||||
|
* this deployment's exact backend version.
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String save(@HeaderParam("X-Api-Key") final String apiKey, @Context final HttpServletRequest servletRequest,
|
||||||
|
final String body) {
|
||||||
|
requireApiKey(apiKey, servletRequest);
|
||||||
|
|
||||||
|
final JsonArray entries = gson.fromJson(body, JsonArray.class);
|
||||||
|
if (entries == null) {
|
||||||
|
throw apiError(400, "Request body must be a JSON array of {menuCode, enabled}.");
|
||||||
|
}
|
||||||
|
final Set<String> touched = new LinkedHashSet<>();
|
||||||
|
for (final var el : entries) {
|
||||||
|
final JsonObject entry = el.getAsJsonObject();
|
||||||
|
if (!entry.has("menuCode") || entry.get("menuCode").isJsonNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String menuCode = entry.get("menuCode").getAsString();
|
||||||
|
if (!KNOWN_MENU_CODES.contains(menuCode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final boolean enabled = entry.has("enabled") && !entry.get("enabled").isJsonNull() && entry.get("enabled").getAsBoolean();
|
||||||
|
jdbcTemplate.update("UPDATE m_tenant_menu_access SET enabled = ? WHERE menu_code = ?", enabled, menuCode);
|
||||||
|
touched.add(menuCode);
|
||||||
|
}
|
||||||
|
final JsonObject res = new JsonObject();
|
||||||
|
res.addProperty("tenant", ThreadLocalContextUtil.getTenant().getTenantIdentifier());
|
||||||
|
res.addProperty("updated", touched.size());
|
||||||
|
return gson.toJson(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** No session fallback, unlike every other API-key-capable endpoint in this codebase -- see the class comment. */
|
||||||
|
private void requireApiKey(final String apiKey, final HttpServletRequest servletRequest) {
|
||||||
|
if (StringUtils.isBlank(apiKey)) {
|
||||||
|
throw apiError(403, "This endpoint is only reachable with a platform operator API key.");
|
||||||
|
}
|
||||||
|
apiClientCredentialService.authenticate(apiKey, resolveClientIp(servletRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveClientIp(final HttpServletRequest request) {
|
||||||
|
for (final String header : IP_HEADER_CANDIDATES) {
|
||||||
|
final String value = request.getHeader(header);
|
||||||
|
if (StringUtils.isNotBlank(value)) {
|
||||||
|
return value.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebApplicationException apiError(final int httpStatus, final String message) {
|
||||||
|
final JsonObject body = new JsonObject();
|
||||||
|
body.addProperty("message", message);
|
||||||
|
body.addProperty("defaultUserMessage", message);
|
||||||
|
body.addProperty("httpStatus", httpStatus);
|
||||||
|
return new WebApplicationException(
|
||||||
|
Response.status(httpStatus).entity(gson.toJson(body)).type(MediaType.APPLICATION_JSON).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,6 +67,8 @@
|
|||||||
<include file="parts/0538_add_branch_pincode.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0538_add_branch_pincode.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0539_seed_cash_exchange_permissions.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0539_seed_cash_exchange_permissions.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0540_unique_active_cash_settlement_per_cashier_day.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0540_unique_active_cash_settlement_per_cashier_day.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0541_create_tenant_menu_access.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0542_split_loan_origination_investment_permissions.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0546_aml_rule_direction_and_cash_scope.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0546_aml_rule_direction_and_cash_scope.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0547_aml_rule_subject_scope.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0547_aml_rule_subject_scope.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0548_sponsor_dd_revoke_request.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0548_sponsor_dd_revoke_request.xml" relativeToChangelogFile="true"/>
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- A bank's own role/permission screen cannot be trusted to keep a nav item hidden: any role
|
||||||
|
holding ALL_FUNCTIONS (a super user, or a role misconfigured to carry it, as Clerk was found
|
||||||
|
to be in two tenants this session) bypasses every MENU_.../SIDEBAR_... permission check on the
|
||||||
|
frontend unconditionally. This table is the hard ceiling that check now also has to clear,
|
||||||
|
so an operator onboarding a new bank can cap which top-nav sections it can EVER show, no
|
||||||
|
matter what a future role in that bank's own tenant grants itself. Deliberately not editable
|
||||||
|
from inside the tenant it governs (see TenantMenuAccessApiResource) - the whole point is that
|
||||||
|
a bank's own admin cannot lift this ceiling on themselves.
|
||||||
|
|
||||||
|
One row per top-nav menu code, all enabled by default so every tenant that already exists
|
||||||
|
keeps behaving exactly as it does today until an operator deliberately narrows one. -->
|
||||||
|
<changeSet id="0541-1" author="cbs-nitesh">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<tableExists tableName="m_tenant_menu_access" />
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_tenant_menu_access">
|
||||||
|
<column name="menu_code" type="VARCHAR(64)">
|
||||||
|
<constraints primaryKey="true" nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="enabled" type="BOOLEAN" defaultValueBoolean="true">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0541-2" author="cbs-nitesh">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_tenant_menu_access</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_INSTITUTION" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="SIDEBAR_CFCFRMS" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_TASKS" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_CASHIER" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_CLERK" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_BRANCH_OPS" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_CLEARING" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_DEPOSITS" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_ADVANCES" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_SHARE" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_NPA" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_GENERAL_LEDGER" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_SUB_LEDGER" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_ACCOUNTING" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_CKYC_MGMT" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_INSURANCE" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_DIGITAL" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_HRM" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_ADMIN" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_RBI" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_REPORTS" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_OTHERS" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_CONFIG_WIZARD" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_CHEQUEBOOK" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_LETTER_MGMT" /></insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!-- Two of the 27 top-nav items were sharing a permission code with their neighbour:
|
||||||
|
Loan Origination rode on MENU_ADVANCES (Advances' own code) and Investment rode on
|
||||||
|
MENU_RBI (RBI's own code), so neither could be shown or hidden independently, on a
|
||||||
|
role or on the new per-tenant menu-access ceiling (0541) alike. Splitting them into their
|
||||||
|
own codes here, and granting the new code to every role that already had the shared one,
|
||||||
|
so this migration changes nothing about who sees what today. It only makes the two
|
||||||
|
pairs independently controllable from this point on. -->
|
||||||
|
<changeSet id="0542-1" author="cbs-nitesh">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'MENU_LOAN_ORIGINATION'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="heading_menu" />
|
||||||
|
<column name="code" value="MENU_LOAN_ORIGINATION" />
|
||||||
|
<column name="entity_name" value="MENU" />
|
||||||
|
<column name="action_name" value="LOAN_ORIGINATION" />
|
||||||
|
<column name="can_maker_checker" valueBoolean="false" />
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0542-2" author="cbs-nitesh">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'MENU_INVESTMENT'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="heading_menu" />
|
||||||
|
<column name="code" value="MENU_INVESTMENT" />
|
||||||
|
<column name="entity_name" value="MENU" />
|
||||||
|
<column name="action_name" value="INVESTMENT" />
|
||||||
|
<column name="can_maker_checker" valueBoolean="false" />
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Preserve today's visibility: any role that already sees Advances also keeps seeing
|
||||||
|
Loan Origination, and any role that already sees RBI also keeps seeing Investment. -->
|
||||||
|
<changeSet id="0542-3" author="cbs-nitesh" runOnChange="true">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p2.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission p1 ON p1.id = rp.permission_id AND p1.code = 'MENU_ADVANCES'
|
||||||
|
JOIN m_permission p2 ON p2.code = 'MENU_LOAN_ORIGINATION'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission existing
|
||||||
|
WHERE existing.role_id = rp.role_id AND existing.permission_id = p2.id)
|
||||||
|
</sql>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p2.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission p1 ON p1.id = rp.permission_id AND p1.code = 'MENU_RBI'
|
||||||
|
JOIN m_permission p2 ON p2.code = 'MENU_INVESTMENT'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission existing
|
||||||
|
WHERE existing.role_id = rp.role_id AND existing.permission_id = p2.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Extend the per-tenant menu-access ceiling (0541) to the two new codes, enabled by default
|
||||||
|
so nothing already deployed there changes either. -->
|
||||||
|
<changeSet id="0542-4" author="cbs-nitesh">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<and>
|
||||||
|
<tableExists tableName="m_tenant_menu_access" />
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_tenant_menu_access WHERE menu_code = 'MENU_LOAN_ORIGINATION'</sqlCheck>
|
||||||
|
</and>
|
||||||
|
</preConditions>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_LOAN_ORIGINATION" /></insert>
|
||||||
|
<insert tableName="m_tenant_menu_access"><column name="menu_code" value="MENU_INVESTMENT" /></insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user