From 91a05761adadb745eae8e4b0b861565867a94c88 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Thu, 17 Sep 2026 15:30:55 +0200 Subject: [PATCH 1/3] Feat: Restrict ApplyRequests to REQUEST_APPLIER role Curator org plus default SUBMIT blocked the dedicated request-applier account. Gate the method on REQUEST_APPLIER only. --- chain-api/src/types/UserProfile.ts | 3 +- .../e2e/__snapshots__/api.spec.ts.snap | 6 +-- chain-cli/chaincode-template/e2e/tokenOps.ts | 23 +++++++++ .../src/apples/__snapshots__/api.spec.ts.snap | 2 +- .../src/pk/__snapshots__/api.spec.ts.snap | 2 +- .../src/token/__snapshots__/api.spec.ts.snap | 2 +- chaincode/src/contracts/GalaContract.ts | 4 +- .../contracts/GalaTransactionRequest.spec.ts | 50 +++++++++++++++++-- .../PublicKeyContract.spec.ts.snap | 2 +- chaincode/src/contracts/authorize.ts | 2 + docs/authorization.md | 4 +- 11 files changed, 84 insertions(+), 16 deletions(-) diff --git a/chain-api/src/types/UserProfile.ts b/chain-api/src/types/UserProfile.ts index ae865bd79c..1abaf2610d 100644 --- a/chain-api/src/types/UserProfile.ts +++ b/chain-api/src/types/UserProfile.ts @@ -23,7 +23,8 @@ export enum UserRole { CURATOR = "CURATOR", REGISTRAR = "REGISTRAR", SUBMIT = "SUBMIT", - EVALUATE = "EVALUATE" + EVALUATE = "EVALUATE", + REQUEST_APPLIER = "REQUEST_APPLIER" } export class UserProfile extends ChainObject { diff --git a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap index 74f9987399..51395b7e6b 100644 --- a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap @@ -8,7 +8,7 @@ exports[`API snapshots Api of AppleContract 1`] = ` "contractVersion": "?.?.?", "methods": [ { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { @@ -1873,7 +1873,7 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { @@ -16541,7 +16541,7 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { diff --git a/chain-cli/chaincode-template/e2e/tokenOps.ts b/chain-cli/chaincode-template/e2e/tokenOps.ts index a0b6c2e74e..b9cb7854f8 100644 --- a/chain-cli/chaincode-template/e2e/tokenOps.ts +++ b/chain-cli/chaincode-template/e2e/tokenOps.ts @@ -17,6 +17,7 @@ import { ApplyRequestsDto, CreateTokenClassDto, FetchBalancesDto, + GetMyProfileDto, GrantAllowanceDto, MintTokenDto, TokenAllowance, @@ -25,6 +26,8 @@ import { TokenInstance, TokenInstanceKey, TransferTokenDto, + UpdateUserRolesDto, + UserRole, asValidUserRef, createValidDTO, createValidSubmitDTO @@ -75,7 +78,27 @@ export async function requestMintTokensToUsers( await applyRequests(client, delayMs); } +async function ensureRequestApplierRole(client: AdminChainClients) { + const profileDto = new GetMyProfileDto(); + profileDto.sign(client.pk.privateKey); + const profile = await client.pk.GetMyProfile(profileDto); + const roles = Array.isArray(profile.Data?.roles) ? (profile.Data.roles as string[]) : []; + + if (roles.includes(UserRole.REQUEST_APPLIER)) { + return; + } + + const updateDto = await createValidSubmitDTO(UpdateUserRolesDto, { + user: client.pk.identityKey, + roles: [...roles, UserRole.REQUEST_APPLIER] + }).signed(client.pk.privateKey); + + const response = await client.pk.submitTransaction("UpdateUserRoles", updateDto); + expect(response).toEqual(transactionSuccess()); +} + export async function applyRequests(client: AdminChainClients, delayMs = 2_500) { + await ensureRequestApplierRole(client); await new Promise((resolve) => setTimeout(resolve, delayMs)); return client.assets.submitTransaction( "ApplyRequests", diff --git a/chain-cli/chaincode-template/src/apples/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/src/apples/__snapshots__/api.spec.ts.snap index bac75bacb8..b4d0dc66f7 100644 --- a/chain-cli/chaincode-template/src/apples/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/src/apples/__snapshots__/api.spec.ts.snap @@ -8,7 +8,7 @@ exports[`AppleContract API should match snapshot 1`] = ` "contractVersion": "?.?.?", "methods": [ { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { diff --git a/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap index b8a2550c0a..d466bb35c6 100644 --- a/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/src/pk/__snapshots__/api.spec.ts.snap @@ -101,7 +101,7 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { diff --git a/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap index 642c1ebc8e..428330fd92 100644 --- a/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap @@ -392,7 +392,7 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { diff --git a/chaincode/src/contracts/GalaContract.ts b/chaincode/src/contracts/GalaContract.ts index 5042dae073..3028f3d5aa 100644 --- a/chaincode/src/contracts/GalaContract.ts +++ b/chaincode/src/contracts/GalaContract.ts @@ -43,7 +43,7 @@ import { getApiMethod, getApiMethods } from "./GalaContractApi"; import { EVALUATE, GalaTransaction, SUBMIT, Submit } from "./GalaTransaction"; import { applySavedRequests, hasPendingApplyRequests } from "./GalaTransactionRequest"; import type { RequestMethodHandler } from "./GalaTransactionRequest"; -import { requireCuratorAuth } from "./authorize"; +import { requireRequestApplierAuth } from "./authorize"; export class BatchWriteLimitExceededError extends ValidationFailedError { constructor(writesLimit: number) { @@ -357,7 +357,7 @@ export abstract class GalaContract extends Contract { in: ApplyRequestsDto, out: { arrayOf: AppliedRequest }, description: "Apply queued internal requests", - ...requireCuratorAuth + ...requireRequestApplierAuth }) public async ApplyRequests(ctx: GalaChainContext, dto: ApplyRequestsDto): Promise { return applySavedRequests(ctx, this.getName(), dto, this.requestMethodHandlers); diff --git a/chaincode/src/contracts/GalaTransactionRequest.spec.ts b/chaincode/src/contracts/GalaTransactionRequest.spec.ts index def410a375..8b7fad0213 100644 --- a/chaincode/src/contracts/GalaTransactionRequest.spec.ts +++ b/chaincode/src/contracts/GalaTransactionRequest.spec.ts @@ -12,28 +12,68 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApplyRequestsDto, GalaChainResponseType } from "@gala-chain/api"; -import { TestChaincode, transactionSuccess } from "@gala-chain/test"; +import { + ApplyRequestsDto, + GalaChainResponseType, + UpdateUserRolesDto, + UserAlias, + UserRole, + createValidSubmitDTO +} from "@gala-chain/api"; +import { TestChaincode, transactionErrorKey, transactionSuccess } from "@gala-chain/test"; import { plainToInstance } from "class-transformer"; import TestGalaContract, { KVDto } from "../__test__/TestGalaContract"; +import { PublicKeyContract } from "./PublicKeyContract"; + +const ADMIN_KEY = process.env.DEV_ADMIN_PRIVATE_KEY as string; +const ADMIN_ALIAS = process.env.DEV_ADMIN_USER_ID as UserAlias; + +async function grantRequestApplier(chaincode: TestChaincode): Promise { + const dto = await createValidSubmitDTO(UpdateUserRolesDto, { + user: ADMIN_ALIAS, + roles: [ + UserRole.CURATOR, + UserRole.EVALUATE, + UserRole.REGISTRAR, + UserRole.REQUEST_APPLIER, + UserRole.SUBMIT + ] + }).signed(ADMIN_KEY); + + const response = await chaincode.invoke("PublicKeyContract:UpdateUserRoles", dto); + expect(response).toEqual(transactionSuccess()); +} describe("ApplyRequests uniqueKey", () => { + it("rejects callers without REQUEST_APPLIER", async () => { + const chaincode = new TestChaincode([TestGalaContract, PublicKeyContract]); + + const applyDto = plainToInstance(ApplyRequestsDto, { + uniqueKey: "apply-uk-denied", + minDelayMs: 0 + }).signed(ADMIN_KEY); + + const applyResponse = await chaincode.invoke("TestGalaContract:ApplyRequests", applyDto.serialize()); + expect(applyResponse).toEqual(transactionErrorKey("MISSING_ROLE")); + }); + it("echoes the queued request uniqueKey on each applied item", async () => { - const chaincode = new TestChaincode([TestGalaContract]); + const chaincode = new TestChaincode([TestGalaContract, PublicKeyContract]); + await grantRequestApplier(chaincode); const requestDto = plainToInstance(KVDto, { key: "queued-key", value: "queued-value", uniqueKey: "request-uk-1" - }).signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); + }).signed(ADMIN_KEY); const requestResponse = await chaincode.invoke("TestGalaContract:RequestPutKv", requestDto.serialize()); expect(requestResponse).toEqual(transactionSuccess({ scheduled: true })); const applyDto = plainToInstance(ApplyRequestsDto, { uniqueKey: "apply-uk-1", minDelayMs: 0 - }).signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); + }).signed(ADMIN_KEY); const applyResponse = await chaincode.invoke("TestGalaContract:ApplyRequests", applyDto.serialize()); expect(applyResponse).toEqual( diff --git a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap index e4e8efdb22..003f5a974b 100644 --- a/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap +++ b/chaincode/src/contracts/__snapshots__/PublicKeyContract.spec.ts.snap @@ -102,7 +102,7 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": "Apply queued internal requests Transaction updates the chain (submit). Allowed roles: REQUEST_APPLIER.", "dtoSchema": { "properties": { "domain": { diff --git a/chaincode/src/contracts/authorize.ts b/chaincode/src/contracts/authorize.ts index b1df679180..5fc38beef8 100644 --- a/chaincode/src/contracts/authorize.ts +++ b/chaincode/src/contracts/authorize.ts @@ -58,6 +58,8 @@ export const requireRegistrarAuth = useRoleBasedAuth ? { allowedRoles: [UserRole.REGISTRAR] } : { allowedOrgs: registrarOrgMsps }; +export const requireRequestApplierAuth = { allowedRoles: [UserRole.REQUEST_APPLIER] }; + export function ensureOrganizationIsAllowed(ctx: GalaChainContext, allowedOrgsMSPs: string[] | undefined) { const userMsp: string = ctx.clientIdentity.getMSPID(); const isAllowed = (allowedOrgsMSPs || []).some((o) => o === userMsp); diff --git a/docs/authorization.md b/docs/authorization.md index 82c10386cc..984640f503 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -422,7 +422,7 @@ The roles are assigned to the `UserProfile` object in the chain data. By default, the `EVALUATE` and `SUBMIT` roles are assigned to the user when they are registered. You can assign additional roles to the user using the `PublicKeyContract:UpdateUserRoles` method. This method requires that the calling user either has the `CURATOR` role or is a CA user from a curator organization. -There are some predefined roles (`EVALUATE`, `SUBMIT`, `CURATOR`, `REGISTRAR`). You can also define custom roles for more granular access control. +There are some predefined roles (`EVALUATE`, `SUBMIT`, `CURATOR`, `REGISTRAR`, `REQUEST_APPLIER`). You can also define custom roles for more granular access control. ### Default Role Assignment @@ -436,6 +436,8 @@ For admin users (when `DEV_ADMIN_PUBLIC_KEY` is set), the following admin roles - `SUBMIT`: Allows submitting transactions that modify state - `REGISTRAR`: Allows registering new users +`ApplyRequests` requires `REQUEST_APPLIER` only. Assign that role with `UpdateUserRoles`; curator or submit access is not enough. + For registration methods, the `REGISTRAR` role is required if RBAC is enabled. ### Using Roles in Contract Methods From a7607f90f1ec879034d6040e103cce49675a7f2e Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Thu, 17 Sep 2026 15:54:18 +0200 Subject: [PATCH 2/3] Refactor: Gate ApplyRequests with a REQUEST_APPLIER string role Drop the extra enum, auth helper, and test setup. The method only needs allowedRoles set on GalaContract. --- chain-api/src/types/UserProfile.ts | 3 +- chain-cli/chaincode-template/e2e/tokenOps.ts | 23 --------- chaincode/src/contracts/GalaContract.ts | 5 +- .../contracts/GalaTransactionRequest.spec.ts | 50 ++----------------- chaincode/src/contracts/authorize.ts | 2 - docs/authorization.md | 4 +- 6 files changed, 10 insertions(+), 77 deletions(-) diff --git a/chain-api/src/types/UserProfile.ts b/chain-api/src/types/UserProfile.ts index 1abaf2610d..ae865bd79c 100644 --- a/chain-api/src/types/UserProfile.ts +++ b/chain-api/src/types/UserProfile.ts @@ -23,8 +23,7 @@ export enum UserRole { CURATOR = "CURATOR", REGISTRAR = "REGISTRAR", SUBMIT = "SUBMIT", - EVALUATE = "EVALUATE", - REQUEST_APPLIER = "REQUEST_APPLIER" + EVALUATE = "EVALUATE" } export class UserProfile extends ChainObject { diff --git a/chain-cli/chaincode-template/e2e/tokenOps.ts b/chain-cli/chaincode-template/e2e/tokenOps.ts index b9cb7854f8..a0b6c2e74e 100644 --- a/chain-cli/chaincode-template/e2e/tokenOps.ts +++ b/chain-cli/chaincode-template/e2e/tokenOps.ts @@ -17,7 +17,6 @@ import { ApplyRequestsDto, CreateTokenClassDto, FetchBalancesDto, - GetMyProfileDto, GrantAllowanceDto, MintTokenDto, TokenAllowance, @@ -26,8 +25,6 @@ import { TokenInstance, TokenInstanceKey, TransferTokenDto, - UpdateUserRolesDto, - UserRole, asValidUserRef, createValidDTO, createValidSubmitDTO @@ -78,27 +75,7 @@ export async function requestMintTokensToUsers( await applyRequests(client, delayMs); } -async function ensureRequestApplierRole(client: AdminChainClients) { - const profileDto = new GetMyProfileDto(); - profileDto.sign(client.pk.privateKey); - const profile = await client.pk.GetMyProfile(profileDto); - const roles = Array.isArray(profile.Data?.roles) ? (profile.Data.roles as string[]) : []; - - if (roles.includes(UserRole.REQUEST_APPLIER)) { - return; - } - - const updateDto = await createValidSubmitDTO(UpdateUserRolesDto, { - user: client.pk.identityKey, - roles: [...roles, UserRole.REQUEST_APPLIER] - }).signed(client.pk.privateKey); - - const response = await client.pk.submitTransaction("UpdateUserRoles", updateDto); - expect(response).toEqual(transactionSuccess()); -} - export async function applyRequests(client: AdminChainClients, delayMs = 2_500) { - await ensureRequestApplierRole(client); await new Promise((resolve) => setTimeout(resolve, delayMs)); return client.assets.submitTransaction( "ApplyRequests", diff --git a/chaincode/src/contracts/GalaContract.ts b/chaincode/src/contracts/GalaContract.ts index 3028f3d5aa..3137ae64ec 100644 --- a/chaincode/src/contracts/GalaContract.ts +++ b/chaincode/src/contracts/GalaContract.ts @@ -43,7 +43,6 @@ import { getApiMethod, getApiMethods } from "./GalaContractApi"; import { EVALUATE, GalaTransaction, SUBMIT, Submit } from "./GalaTransaction"; import { applySavedRequests, hasPendingApplyRequests } from "./GalaTransactionRequest"; import type { RequestMethodHandler } from "./GalaTransactionRequest"; -import { requireRequestApplierAuth } from "./authorize"; export class BatchWriteLimitExceededError extends ValidationFailedError { constructor(writesLimit: number) { @@ -64,6 +63,8 @@ export class BatchPartialSuccessRequiredError extends ChainError { } } +const REQUEST_APPLIER_ROLE = "REQUEST_APPLIER"; + export abstract class GalaContract extends Contract { protected readonly requestMethodHandlers: Record = {}; @@ -357,7 +358,7 @@ export abstract class GalaContract extends Contract { in: ApplyRequestsDto, out: { arrayOf: AppliedRequest }, description: "Apply queued internal requests", - ...requireRequestApplierAuth + allowedRoles: [REQUEST_APPLIER_ROLE] }) public async ApplyRequests(ctx: GalaChainContext, dto: ApplyRequestsDto): Promise { return applySavedRequests(ctx, this.getName(), dto, this.requestMethodHandlers); diff --git a/chaincode/src/contracts/GalaTransactionRequest.spec.ts b/chaincode/src/contracts/GalaTransactionRequest.spec.ts index 8b7fad0213..def410a375 100644 --- a/chaincode/src/contracts/GalaTransactionRequest.spec.ts +++ b/chaincode/src/contracts/GalaTransactionRequest.spec.ts @@ -12,68 +12,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - ApplyRequestsDto, - GalaChainResponseType, - UpdateUserRolesDto, - UserAlias, - UserRole, - createValidSubmitDTO -} from "@gala-chain/api"; -import { TestChaincode, transactionErrorKey, transactionSuccess } from "@gala-chain/test"; +import { ApplyRequestsDto, GalaChainResponseType } from "@gala-chain/api"; +import { TestChaincode, transactionSuccess } from "@gala-chain/test"; import { plainToInstance } from "class-transformer"; import TestGalaContract, { KVDto } from "../__test__/TestGalaContract"; -import { PublicKeyContract } from "./PublicKeyContract"; - -const ADMIN_KEY = process.env.DEV_ADMIN_PRIVATE_KEY as string; -const ADMIN_ALIAS = process.env.DEV_ADMIN_USER_ID as UserAlias; - -async function grantRequestApplier(chaincode: TestChaincode): Promise { - const dto = await createValidSubmitDTO(UpdateUserRolesDto, { - user: ADMIN_ALIAS, - roles: [ - UserRole.CURATOR, - UserRole.EVALUATE, - UserRole.REGISTRAR, - UserRole.REQUEST_APPLIER, - UserRole.SUBMIT - ] - }).signed(ADMIN_KEY); - - const response = await chaincode.invoke("PublicKeyContract:UpdateUserRoles", dto); - expect(response).toEqual(transactionSuccess()); -} describe("ApplyRequests uniqueKey", () => { - it("rejects callers without REQUEST_APPLIER", async () => { - const chaincode = new TestChaincode([TestGalaContract, PublicKeyContract]); - - const applyDto = plainToInstance(ApplyRequestsDto, { - uniqueKey: "apply-uk-denied", - minDelayMs: 0 - }).signed(ADMIN_KEY); - - const applyResponse = await chaincode.invoke("TestGalaContract:ApplyRequests", applyDto.serialize()); - expect(applyResponse).toEqual(transactionErrorKey("MISSING_ROLE")); - }); - it("echoes the queued request uniqueKey on each applied item", async () => { - const chaincode = new TestChaincode([TestGalaContract, PublicKeyContract]); - await grantRequestApplier(chaincode); + const chaincode = new TestChaincode([TestGalaContract]); const requestDto = plainToInstance(KVDto, { key: "queued-key", value: "queued-value", uniqueKey: "request-uk-1" - }).signed(ADMIN_KEY); + }).signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); const requestResponse = await chaincode.invoke("TestGalaContract:RequestPutKv", requestDto.serialize()); expect(requestResponse).toEqual(transactionSuccess({ scheduled: true })); const applyDto = plainToInstance(ApplyRequestsDto, { uniqueKey: "apply-uk-1", minDelayMs: 0 - }).signed(ADMIN_KEY); + }).signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); const applyResponse = await chaincode.invoke("TestGalaContract:ApplyRequests", applyDto.serialize()); expect(applyResponse).toEqual( diff --git a/chaincode/src/contracts/authorize.ts b/chaincode/src/contracts/authorize.ts index 5fc38beef8..b1df679180 100644 --- a/chaincode/src/contracts/authorize.ts +++ b/chaincode/src/contracts/authorize.ts @@ -58,8 +58,6 @@ export const requireRegistrarAuth = useRoleBasedAuth ? { allowedRoles: [UserRole.REGISTRAR] } : { allowedOrgs: registrarOrgMsps }; -export const requireRequestApplierAuth = { allowedRoles: [UserRole.REQUEST_APPLIER] }; - export function ensureOrganizationIsAllowed(ctx: GalaChainContext, allowedOrgsMSPs: string[] | undefined) { const userMsp: string = ctx.clientIdentity.getMSPID(); const isAllowed = (allowedOrgsMSPs || []).some((o) => o === userMsp); diff --git a/docs/authorization.md b/docs/authorization.md index 984640f503..82c10386cc 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -422,7 +422,7 @@ The roles are assigned to the `UserProfile` object in the chain data. By default, the `EVALUATE` and `SUBMIT` roles are assigned to the user when they are registered. You can assign additional roles to the user using the `PublicKeyContract:UpdateUserRoles` method. This method requires that the calling user either has the `CURATOR` role or is a CA user from a curator organization. -There are some predefined roles (`EVALUATE`, `SUBMIT`, `CURATOR`, `REGISTRAR`, `REQUEST_APPLIER`). You can also define custom roles for more granular access control. +There are some predefined roles (`EVALUATE`, `SUBMIT`, `CURATOR`, `REGISTRAR`). You can also define custom roles for more granular access control. ### Default Role Assignment @@ -436,8 +436,6 @@ For admin users (when `DEV_ADMIN_PUBLIC_KEY` is set), the following admin roles - `SUBMIT`: Allows submitting transactions that modify state - `REGISTRAR`: Allows registering new users -`ApplyRequests` requires `REQUEST_APPLIER` only. Assign that role with `UpdateUserRoles`; curator or submit access is not enough. - For registration methods, the `REGISTRAR` role is required if RBAC is enabled. ### Using Roles in Contract Methods From a070604d035b1cd33956f7f4a5c240e060572aee Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Thu, 17 Sep 2026 16:12:40 +0200 Subject: [PATCH 3/3] Test: Call ApplyRequests as a user with REQUEST_APPLIER Admin no longer has that role, so unit and template e2e setup grant it to the caller first. --- chain-cli/chaincode-template/e2e/tokenOps.ts | 14 +++++++++++++ .../contracts/GalaTransactionRequest.spec.ts | 21 ++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/chain-cli/chaincode-template/e2e/tokenOps.ts b/chain-cli/chaincode-template/e2e/tokenOps.ts index a0b6c2e74e..5f56e12757 100644 --- a/chain-cli/chaincode-template/e2e/tokenOps.ts +++ b/chain-cli/chaincode-template/e2e/tokenOps.ts @@ -17,6 +17,7 @@ import { ApplyRequestsDto, CreateTokenClassDto, FetchBalancesDto, + GetMyProfileDto, GrantAllowanceDto, MintTokenDto, TokenAllowance, @@ -25,6 +26,7 @@ import { TokenInstance, TokenInstanceKey, TransferTokenDto, + UpdateUserRolesDto, asValidUserRef, createValidDTO, createValidSubmitDTO @@ -76,6 +78,18 @@ export async function requestMintTokensToUsers( } export async function applyRequests(client: AdminChainClients, delayMs = 2_500) { + const profileDto = new GetMyProfileDto(); + profileDto.sign(client.pk.privateKey); + const profile = await client.pk.GetMyProfile(profileDto); + const roles = Array.isArray(profile.Data?.roles) ? (profile.Data.roles as string[]) : []; + if (!roles.includes("REQUEST_APPLIER")) { + const grantRoles = await createValidSubmitDTO(UpdateUserRolesDto, { + user: client.pk.identityKey, + roles: [...roles, "REQUEST_APPLIER"] + }).signed(client.pk.privateKey); + expect(await client.pk.submitTransaction("UpdateUserRoles", grantRoles)).toEqual(transactionSuccess()); + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); return client.assets.submitTransaction( "ApplyRequests", diff --git a/chaincode/src/contracts/GalaTransactionRequest.spec.ts b/chaincode/src/contracts/GalaTransactionRequest.spec.ts index def410a375..501b378efb 100644 --- a/chaincode/src/contracts/GalaTransactionRequest.spec.ts +++ b/chaincode/src/contracts/GalaTransactionRequest.spec.ts @@ -12,15 +12,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApplyRequestsDto, GalaChainResponseType } from "@gala-chain/api"; +import { + ApplyRequestsDto, + GalaChainResponseType, + UpdateUserRolesDto, + createValidSubmitDTO +} from "@gala-chain/api"; import { TestChaincode, transactionSuccess } from "@gala-chain/test"; import { plainToInstance } from "class-transformer"; import TestGalaContract, { KVDto } from "../__test__/TestGalaContract"; +import { PublicKeyContract } from "./PublicKeyContract"; +import { createRegisteredUser } from "./authenticate.testutils.spec"; describe("ApplyRequests uniqueKey", () => { it("echoes the queued request uniqueKey on each applied item", async () => { - const chaincode = new TestChaincode([TestGalaContract]); + const chaincode = new TestChaincode([TestGalaContract, PublicKeyContract]); + const applier = await createRegisteredUser(chaincode); + const grantRoles = await createValidSubmitDTO(UpdateUserRolesDto, { + user: applier.alias, + roles: ["REQUEST_APPLIER"] + }).signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); + expect(await chaincode.invoke("PublicKeyContract:UpdateUserRoles", grantRoles)).toEqual( + transactionSuccess() + ); const requestDto = plainToInstance(KVDto, { key: "queued-key", @@ -33,7 +48,7 @@ describe("ApplyRequests uniqueKey", () => { const applyDto = plainToInstance(ApplyRequestsDto, { uniqueKey: "apply-uk-1", minDelayMs: 0 - }).signed(process.env.DEV_ADMIN_PRIVATE_KEY as string); + }).signed(applier.privateKey); const applyResponse = await chaincode.invoke("TestGalaContract:ApplyRequests", applyDto.serialize()); expect(applyResponse).toEqual(