Skip to content

Commit e3f3a62

Browse files
committed
feat: 🎸 add field to expose unmatched permissions
1 parent 8403415 commit e3f3a62

File tree

8 files changed

+185
-24
lines changed

8 files changed

+185
-24
lines changed

src/api/entities/CustomPermissionGroup.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@ export class CustomPermissionGroup extends PermissionGroup {
7979

8080
const rawGroupPermissions = await externalAgents.groupPermissions(rawAssetId, rawAgId);
8181

82-
const transactions = extrinsicPermissionsToTransactionPermissions(rawGroupPermissions.unwrap());
82+
const { permissions } = extrinsicPermissionsToTransactionPermissions(
83+
rawGroupPermissions.unwrap()
84+
);
8385

84-
const transactionGroups = transactionPermissionsToTxGroups(transactions);
86+
const transactionGroups = transactionPermissionsToTxGroups(permissions);
8587

8688
return {
87-
transactions,
89+
transactions: permissions,
8890
transactionGroups,
8991
};
9092
}

src/api/entities/Identity/AssetPermissions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class AssetPermissions extends Namespace<Identity> {
182182

183183
const groupPermissionsOption = await externalAgents.groupPermissions(rawAssetId, groupId);
184184

185-
const permissions = extrinsicPermissionsToTransactionPermissions(
185+
const { permissions } = extrinsicPermissionsToTransactionPermissions(
186186
groupPermissionsOption.unwrap()
187187
);
188188

src/api/entities/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@ export interface CheckpointWithData {
773773
export interface PermissionedAccount {
774774
account: Account | MultiSig;
775775
permissions: Permissions;
776+
unmatchedPermissions?: string[];
776777
}
777778

778779
export type PortfolioLike =

src/generated/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -983,3 +983,9 @@ export const TxTags = {
983983
stateTrieMigration: StateTrieMigrationTx,
984984
electionProviderMultiPhase: ElectionProviderMultiPhaseTx,
985985
};
986+
987+
export const TX_TAG_VALUES: string[] = Object.values(TxTags)
988+
.map(v => Object.values(v))
989+
.flat();
990+
991+
export const MODULE_NAMES: string[] = Object.values(ModuleName);

src/utils/__tests__/conversion.ts

+85-4
Original file line numberDiff line numberDiff line change
@@ -2277,7 +2277,8 @@ describe('permissionsToMeshPermissions and meshPermissionsToPermissions', () =>
22772277
entityMockUtils.getAccountInstance(),
22782278
context
22792279
);
2280-
expect(result).toEqual(fakeResult);
2280+
expect(result.permissions).toEqual(fakeResult);
2281+
expect(result.unmatchedPermissions).toEqual([]);
22812282

22822283
fakeResult = {
22832284
assets: null,
@@ -2295,7 +2296,8 @@ describe('permissionsToMeshPermissions and meshPermissionsToPermissions', () =>
22952296
]);
22962297

22972298
result = await meshPermissionsToPermissionsV2(entityMockUtils.getMultiSigInstance(), context);
2298-
expect(result).toEqual(fakeResult);
2299+
expect(result.permissions).toEqual(fakeResult);
2300+
expect(result.unmatchedPermissions).toEqual([]);
22992301

23002302
fakeResult = {
23012303
assets: {
@@ -2347,13 +2349,16 @@ describe('permissionsToMeshPermissions and meshPermissionsToPermissions', () =>
23472349
]);
23482350

23492351
result = await meshPermissionsToPermissionsV2(entityMockUtils.getAccountInstance(), context);
2350-
expect(result).toEqual(fakeResult);
2352+
expect(result.permissions).toEqual(fakeResult);
2353+
expect(result.unmatchedPermissions).toEqual([]);
23512354
});
23522355

23532356
it('should filter out incorrectly cased modules and extrinsics', async () => {
23542357
const context = dsMockUtils.getContextInstance();
23552358
const rawIdentityName = dsMockUtils.createMockText('identity');
23562359
const rawAuthorshipName = dsMockUtils.createMockText('Asset');
2360+
const rawUnknownModuleName = dsMockUtils.createMockText('UnknownModule');
2361+
const rawKnownModuleName = dsMockUtils.createMockText('Checkpoint');
23572362

23582363
const rawIdentityPermissions = dsMockUtils.createMockPalletPermissions({
23592364
extrinsics: dsMockUtils.createMockExtrinsicName({
@@ -2367,9 +2372,74 @@ describe('permissionsToMeshPermissions and meshPermissionsToPermissions', () =>
23672372
}),
23682373
});
23692374

2375+
const rawWholePermissions = dsMockUtils.createMockPalletPermissions({
2376+
extrinsics: dsMockUtils.createMockExtrinsicName('Whole'),
2377+
});
2378+
2379+
const rawUnknownExtrinsicPermissions = dsMockUtils.createMockPalletPermissions({
2380+
extrinsics: dsMockUtils.createMockExtrinsicName({
2381+
These: [dsMockUtils.createMockText('unknown_extrinsic')],
2382+
}),
2383+
});
2384+
23702385
const permissionsMap = new Map();
23712386
permissionsMap.set(rawIdentityName, rawIdentityPermissions);
23722387
permissionsMap.set(rawAuthorshipName, rawAssetPermissions);
2388+
permissionsMap.set(rawUnknownModuleName, rawWholePermissions);
2389+
permissionsMap.set(rawKnownModuleName, rawUnknownExtrinsicPermissions);
2390+
2391+
dsMockUtils.getQueryMultiMock().mockResolvedValue([
2392+
dsMockUtils.createMockOption(),
2393+
dsMockUtils.createMockOption(
2394+
dsMockUtils.createMockExtrinsicPermissions({
2395+
These: dsMockUtils.createMockBTreeMap(permissionsMap),
2396+
})
2397+
),
2398+
dsMockUtils.createMockOption(),
2399+
]);
2400+
2401+
const result = await meshPermissionsToPermissionsV2(
2402+
entityMockUtils.getAccountInstance(),
2403+
context
2404+
);
2405+
// we are not filtering out the unmatched extrinsics
2406+
expect(result.permissions?.transactions?.values).toEqual([
2407+
'unknownModule',
2408+
'checkpoint.unknownExtrinsic',
2409+
]);
2410+
expect(result.unmatchedPermissions).toEqual([
2411+
'identity',
2412+
'Asset.createAsset',
2413+
// since the module and extrinsic follow the same naming convention, they are converted to lowercase and camel case
2414+
'unknownModule',
2415+
'checkpoint.unknownExtrinsic',
2416+
]);
2417+
});
2418+
2419+
it('should handle except case for dispatchable names', async () => {
2420+
const context = dsMockUtils.getContextInstance();
2421+
const rawIdentityName = dsMockUtils.createMockText('Identity');
2422+
const rawAssetName = dsMockUtils.createMockText('Asset');
2423+
2424+
// Create mock permissions with except case
2425+
const rawIdentityPermissions = dsMockUtils.createMockPalletPermissions({
2426+
extrinsics: dsMockUtils.createMockExtrinsicName({
2427+
Except: [
2428+
dsMockUtils.createMockText('add_claim'), // valid
2429+
dsMockUtils.createMockText('some_tag'),
2430+
],
2431+
}),
2432+
});
2433+
2434+
const rawAssetPermissions = dsMockUtils.createMockPalletPermissions({
2435+
extrinsics: dsMockUtils.createMockExtrinsicName({
2436+
Except: [dsMockUtils.createMockText('invalid_Tx')],
2437+
}),
2438+
});
2439+
2440+
const permissionsMap = new Map();
2441+
permissionsMap.set(rawIdentityName, rawIdentityPermissions);
2442+
permissionsMap.set(rawAssetName, rawAssetPermissions);
23732443

23742444
dsMockUtils.getQueryMultiMock().mockResolvedValue([
23752445
dsMockUtils.createMockOption(),
@@ -2385,7 +2455,18 @@ describe('permissionsToMeshPermissions and meshPermissionsToPermissions', () =>
23852455
entityMockUtils.getAccountInstance(),
23862456
context
23872457
);
2388-
expect(result.transactions?.values).toEqual([]);
2458+
2459+
// Verify exceptions are properly handled
2460+
expect(result.permissions?.transactions?.exceptions).toEqual([
2461+
'identity.addClaim',
2462+
'identity.someTag',
2463+
]);
2464+
2465+
// Verify unmatched permissions are properly concatenated with module name
2466+
expect(result.unmatchedPermissions).toEqual(['Asset.invalid_Tx', 'identity.someTag']);
2467+
2468+
// Verify module level permissions
2469+
expect(result.permissions?.transactions?.values).toEqual(['identity', 'asset']);
23892470
});
23902471
});
23912472
});

src/utils/__tests__/internal.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1942,6 +1942,7 @@ describe('method: getSecondaryAccountPermissions', () => {
19421942
transactions: null,
19431943
transactionGroups: [],
19441944
},
1945+
unmatchedPermissions: [],
19451946
},
19461947
];
19471948

@@ -1956,10 +1957,13 @@ describe('method: getSecondaryAccountPermissions', () => {
19561957
});
19571958

19581959
meshPermissionsToPermissionsSpy.mockReturnValue({
1959-
assets: null,
1960-
portfolios: null,
1961-
transactions: null,
1962-
transactionGroups: [],
1960+
permissions: {
1961+
assets: null,
1962+
portfolios: null,
1963+
transactions: null,
1964+
transactionGroups: [],
1965+
},
1966+
unmatchedPermissions: [],
19631967
});
19641968
stringToAccountIdSpy.mockReturnValue(dsMockUtils.createMockAccountId(accountId));
19651969
});

src/utils/conversion.ts

+72-11
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ import {
227227
MetadataType,
228228
MetadataValue,
229229
MetadataValueDetails,
230+
MODULE_NAMES,
230231
ModuleName,
231232
MultiClaimCondition,
232233
NftMetadataInput,
@@ -274,6 +275,7 @@ import {
274275
TransferRestrictionType,
275276
TransferStatus,
276277
TrustedClaimIssuer,
278+
TX_TAG_VALUES,
277279
TxGroup,
278280
TxTag,
279281
TxTags,
@@ -1163,16 +1165,26 @@ const formatTxTag = (dispatchable: string, moduleName: string): TxTag => {
11631165
return `${moduleName}.${camelCase(dispatchable)}` as TxTag;
11641166
};
11651167

1166-
const processDispatchName = (dispatch: BTreeSet<Text>): string[] => {
1167-
return [...dispatch].map(name => textToString(name)).filter(name => isSnakeCase(name));
1168+
const processDispatchName = (
1169+
dispatch: BTreeSet<Text>
1170+
): {
1171+
dispatchables: string[];
1172+
unmatchedTags: string[];
1173+
} => {
1174+
const allMethods = [...dispatch].map(name => textToString(name));
1175+
1176+
return {
1177+
dispatchables: allMethods.filter(name => isSnakeCase(name)),
1178+
unmatchedTags: allMethods.filter(name => !isSnakeCase(name)),
1179+
};
11681180
};
11691181

11701182
/**
11711183
* @hidden
11721184
*/
11731185
export function extrinsicPermissionsToTransactionPermissions(
11741186
permissions: PolymeshPrimitivesSecondaryKeyExtrinsicPermissions
1175-
): TransactionPermissions | null {
1187+
): { permissions: TransactionPermissions | null; unmatched: string[] } {
11761188
let extrinsicType: PermissionType;
11771189
let pallets;
11781190
if (permissions.isThese) {
@@ -1185,23 +1197,36 @@ export function extrinsicPermissionsToTransactionPermissions(
11851197

11861198
let txValues: (ModuleName | TxTag)[] = [];
11871199
let exceptions: TxTag[] = [];
1200+
const unmatched: string[] = [];
11881201

11891202
if (pallets) {
11901203
// Note if a pallet or extrinsic has incorrect casing it will get filtered here
11911204
pallets.forEach(({ extrinsics: dispatchableNames }, palletName) => {
11921205
const pallet = textToString(palletName);
1206+
11931207
if (!startsWithCapital(pallet)) {
1208+
unmatched.push(pallet);
1209+
11941210
return; // skip incorrect cased pallets
11951211
}
11961212
const moduleName = stringLowerFirst(pallet);
11971213

11981214
if (dispatchableNames.isExcept) {
1199-
const dispatchables = processDispatchName(dispatchableNames.asExcept);
1215+
const { dispatchables, unmatchedTags } = processDispatchName(dispatchableNames.asExcept);
1216+
1217+
if (unmatchedTags.length) {
1218+
unmatched.push(`${pallet}.${unmatchedTags.join('.')}`);
1219+
}
12001220

12011221
exceptions = [...exceptions, ...dispatchables.map(name => formatTxTag(name, moduleName))];
12021222
txValues = [...txValues, moduleName as ModuleName];
12031223
} else if (dispatchableNames.isThese) {
1204-
const dispatchables = processDispatchName(dispatchableNames.asThese);
1224+
const { dispatchables, unmatchedTags } = processDispatchName(dispatchableNames.asThese);
1225+
1226+
if (unmatchedTags.length) {
1227+
unmatched.push(`${pallet}.${unmatchedTags.join('.')}`);
1228+
}
1229+
12051230
txValues = [...txValues, ...dispatchables.map(name => formatTxTag(name, moduleName))];
12061231
} else {
12071232
txValues = [...txValues, moduleName as ModuleName];
@@ -1214,10 +1239,10 @@ export function extrinsicPermissionsToTransactionPermissions(
12141239
values: txValues,
12151240
};
12161241

1217-
return exceptions.length ? { ...result, exceptions } : result;
1242+
return { permissions: exceptions.length ? { ...result, exceptions } : result, unmatched };
12181243
}
12191244

1220-
return null;
1245+
return { permissions: null, unmatched };
12211246
}
12221247

12231248
/**
@@ -1253,7 +1278,8 @@ export function meshPermissionsToPermissions(
12531278
};
12541279
}
12551280

1256-
transactions = extrinsicPermissionsToTransactionPermissions(extrinsic);
1281+
const { permissions: transactionPerms } = extrinsicPermissionsToTransactionPermissions(extrinsic);
1282+
transactions = transactionPerms;
12571283

12581284
let portfoliosType: PermissionType;
12591285
let portfolioIds;
@@ -1289,14 +1315,15 @@ export function meshPermissionsToPermissions(
12891315
export async function meshPermissionsToPermissionsV2(
12901316
account: Account | MultiSig,
12911317
context: Context
1292-
): Promise<Permissions> {
1318+
): Promise<{ permissions: Permissions; unmatchedPermissions: string[] }> {
12931319
const {
12941320
polymeshApi: {
12951321
query: {
12961322
identity: { keyAssetPermissions, keyExtrinsicPermissions, keyPortfolioPermissions },
12971323
},
12981324
},
12991325
} = context;
1326+
const unmatchedPermissions: string[] = [];
13001327

13011328
const rawAccountId = stringToAccountId(account.address, context);
13021329

@@ -1338,7 +1365,15 @@ export async function meshPermissionsToPermissionsV2(
13381365
}
13391366

13401367
if (extrinsic.isSome) {
1341-
transactions = extrinsicPermissionsToTransactionPermissions(extrinsic.unwrap());
1368+
const { permissions, unmatched } = extrinsicPermissionsToTransactionPermissions(
1369+
extrinsic.unwrap()
1370+
);
1371+
1372+
transactions = permissions;
1373+
1374+
if (unmatched.length) {
1375+
unmatchedPermissions.push(...unmatched);
1376+
}
13421377
}
13431378

13441379
let portfoliosType: PermissionType;
@@ -1364,12 +1399,37 @@ export async function meshPermissionsToPermissionsV2(
13641399
};
13651400
}
13661401

1367-
return {
1402+
// get transaction permissions values and check for undefined tx tags
1403+
transactions?.values.forEach(value => {
1404+
if (value.includes('.') && !TX_TAG_VALUES.includes(value)) {
1405+
unmatchedPermissions.push(value);
1406+
}
1407+
1408+
if (!value.includes('.') && !MODULE_NAMES.includes(value)) {
1409+
unmatchedPermissions.push(value);
1410+
}
1411+
});
1412+
1413+
if (transactions?.exceptions) {
1414+
transactions.exceptions.forEach(value => {
1415+
if (!TX_TAG_VALUES.includes(value)) {
1416+
unmatchedPermissions.push(value);
1417+
}
1418+
});
1419+
}
1420+
1421+
// Current permission conversion logic
1422+
const permissions = {
13681423
assets,
13691424
transactions,
13701425
transactionGroups: transactions ? transactionPermissionsToTxGroups(transactions) : [],
13711426
portfolios,
13721427
};
1428+
1429+
return {
1430+
permissions,
1431+
unmatchedPermissions,
1432+
};
13731433
}
13741434

13751435
/**
@@ -5541,6 +5601,7 @@ export function secondaryAccountWithAuthToSecondaryKeyWithAuth(
55415601
{
55425602
account: asAccount(account, context),
55435603
permissions: permissionsLikeToPermissions(permissions, context),
5604+
unmatchedPermissions: [],
55445605
},
55455606
context
55465607
),

0 commit comments

Comments
 (0)