diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 4e739e895..73e2f73f4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -165,9 +165,11 @@ public class SecurityConfig { // envelope's RSA signature/encryption (NpciCryptoUtil) inside the handler. .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/npci/**")).permitAll() // Client account summary, third-party callers — a partner integration calls this - // with an X-Api-Key header instead of a Fineract username/password (issued via - // POST /v1/apiclients, super-user only). Same posture as IMPS/NPCI above: the - // outer gate stays open for this one narrow GET path, and + // with an X-Api-Key header instead of a Fineract username/password. The key itself + // is issued by inserting directly into m_api_client in the tenant-store database + // (see ApiClientAdminApiResource — its POST /v1/apiclients is deliberately disabled, + // since any bank's own super user reaching it could mint a platform-wide key). Same + // posture as IMPS/NPCI above: the outer gate stays open for this one narrow GET path, and // ClientAllAccountsApiResource#retrieveByClientNo is what actually authenticates // the caller (ApiClientCredentialService checks the key's hash, active flag and // IP allowlist) — a request with no key and no Fineract login still gets nothing, diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/apiclient/ApiClientAdminApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/apiclient/ApiClientAdminApiResource.java index 0c469621a..a6bca7aa2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/apiclient/ApiClientAdminApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/apiclient/ApiClientAdminApiResource.java @@ -20,7 +20,6 @@ package org.apache.fineract.infrastructure.security.apiclient; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -35,17 +34,26 @@ import jakarta.ws.rs.core.Response; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.stereotype.Component; /** * Admin-only management of the third-party API keys {@link ApiClientCredentialService} authenticates. - * This resource itself requires a normal Fineract login, same as everything else -- ordinary - * username/password, restricted to a super user (ALL_FUNCTIONS). Only the endpoints that use a minted - * key to skip login (e.g. GET /v1/clientaccounts) are the ones exempted in SecurityConfig; issuing that - * key in the first place is not something to hand to just anyone. + * List/deactivate/reactivate still require a normal Fineract login restricted to a super user + * (ALL_FUNCTIONS) -- but minting a NEW key ({@link #create}) is disabled here entirely, on purpose. + * + * {@code ALL_FUNCTIONS} is a per-TENANT role grant -- and {@code TenantMenuAccessApiResource}'s whole + * point is that a bank's own super user (or a role simply misconfigured to carry ALL_FUNCTIONS, as the + * "Clerk" role was found to in two tenants this session) must never be able to lift its own menu-access + * ceiling. If this endpoint minted keys for any ALL_FUNCTIONS holder, that same bank super user could + * call {@code POST /v1/apiclients} and hand themselves a brand-new platform-operator credential -- one + * that authenticates against every tenant, not just their own -- completely bypassing the ceiling this + * key mechanism exists to enforce. A super user being able to read/deactivate the client list is fine + * (visibility and revocation are not privilege escalation); being able to mint a fresh one is exactly + * the escalation path being closed. New keys are issued the same way this fix issued the first one: a + * direct insert into {@code m_api_client} (see {@link ApiClientCredentialService#createClient}), by + * whoever already has direct database access -- never over HTTP. */ @Path("/v1/apiclients") @Component @@ -55,20 +63,18 @@ public class ApiClientAdminApiResource { private final PlatformSecurityContext context; private final ApiClientCredentialService credentialService; - private final FromJsonHelper fromJsonHelper; private final Gson gson = new GsonBuilder().serializeNulls().create(); + /** Disabled -- see the class comment. Minting a key is a direct-database operation only. */ @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public String create(final String body) { - final AppUser user = requireSuperUser(); - final JsonElement root = fromJsonHelper.parse(body); - final String clientName = fromJsonHelper.extractStringNamed("clientName", root); - final String allowedIps = fromJsonHelper.extractStringNamed("allowedIps", root); - - final Map result = credentialService.createClient(clientName, allowedIps, user.getId()); - return gson.toJson(result); + requireSuperUser(); + throw new WebApplicationException(Response.status(403) + .entity("{\"message\":\"Minting a new platform API key over HTTP is disabled. " + + "Insert directly into m_api_client in the tenant-store database instead.\"}") + .type(MediaType.APPLICATION_JSON).build()); } @GET diff --git a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml index 852255252..0c4578281 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml @@ -32,4 +32,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0012_seed_default_platform_api_client.xml b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0012_seed_default_platform_api_client.xml new file mode 100644 index 000000000..a75be07a2 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0012_seed_default_platform_api_client.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + SELECT COUNT(*) FROM m_api_client + WHERE api_key_hash = '9d6d4dfa7a1953e33e0acde4fd775aad11689ec540340c7224796447118a3edd' + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index 265f2c371..d1385c84d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -145,7 +145,6 @@ -<<<<<<< HEAD