diff --git a/apps/authz/Makefile b/apps/authz/Makefile index ca4f8442c..bc8dc6486 100644 --- a/apps/authz/Makefile +++ b/apps/authz/Makefile @@ -11,7 +11,7 @@ authz/start/dev: authz/setup: make authz/copy-default-env - make authz/rego/compile + make authz/rego/build authz/copy-default-env: cp ${AUTHZ_PROJECT_DIR}/.env.default ${AUTHZ_PROJECT_DIR}/.env @@ -90,3 +90,8 @@ authz/rego/test: authz/rego/test/watch: make authz/rego/test ARGS=--watch + +authz/rego/translate-legacy-policy: + npx dotenv -e ${AUTHZ_PROJECT_DIR}/.env -- \ + ts-node -r tsconfig-paths/register \ + --project ${AUTHZ_PROJECT_DIR}/tsconfig.app.json ${AUTHZ_PROJECT_DIR}/src/opa/template/translate-legacy-policy.script.ts diff --git a/apps/authz/src/opa/rego/lib/criteria/transactionRequest/chainId.rego b/apps/authz/src/opa/rego/lib/criteria/transactionRequest/chainId.rego new file mode 100644 index 000000000..fbe0a4e48 --- /dev/null +++ b/apps/authz/src/opa/rego/lib/criteria/transactionRequest/chainId.rego @@ -0,0 +1,9 @@ +package main + +import future.keywords.in + +chainId = numberToString(input.transactionRequest.chainId) + +checkChainId(values) { + chainId in values +} diff --git a/apps/authz/src/opa/rego/lib/criteria/transactionRequest/gas.rego b/apps/authz/src/opa/rego/lib/criteria/transactionRequest/gas.rego index cae57a39e..ce982e346 100644 --- a/apps/authz/src/opa/rego/lib/criteria/transactionRequest/gas.rego +++ b/apps/authz/src/opa/rego/lib/criteria/transactionRequest/gas.rego @@ -11,7 +11,6 @@ gasFeeAmount(currency) = result { gasFeeAmount(currency) = result { currency != wildcard - chainId = numberToString(input.transactionRequest.chainId) token = chainAssetId[chainId] price = to_number(priceFeed[token][currency]) result = gasFee * price diff --git a/apps/authz/src/opa/script/translate-legacy-policy.script.ts b/apps/authz/src/opa/script/translate-legacy-policy.script.ts new file mode 100644 index 000000000..cd3ec805c --- /dev/null +++ b/apps/authz/src/opa/script/translate-legacy-policy.script.ts @@ -0,0 +1,349 @@ +import { EntityType, FiatCurrency, UserRole, ValueOperators } from '@narval/authz-shared' +import axios from 'axios' +import { omit } from 'lodash' +import { Address, Hex } from 'viem' +import { + ApprovalCondition, + Criterion, + Policy, + PolicyCriterion, + SignTypedDataDomainCondition, + Then +} from '../../shared/types/policy.type' + +type OldPolicy = { [key: string]: string | null } + +type NewPolicy = Policy & { id: string } + +const translateLegacyPolicy = (oldPolicy: OldPolicy): NewPolicy | null => { + const { + id, + result, + chain_id, + assetType, + asset_contract_address: assetAddress, + asset_token_id: assetTokenId, + destination_account_type, + destination_address, + signing_type, + usd_amount, + comparison_operator, + domain_version, + domain_name, + domain_verifying_contract, + approval_threshold, + approval_user_id, + approval_user_role, + approval_user_group + } = oldPolicy + + if (!id || !result) { + return null + } + + const res: NewPolicy = { + id, + name: id, + when: [ + { + criterion: Criterion.CHECK_RESOURCE_INTEGRITY, + args: null + }, + { + criterion: Criterion.CHECK_NONCE_EXISTS, + args: null + } + ], + then: ['approve', 'confirm'].includes(result) ? Then.PERMIT : Then.FORBID + } + + const chainId = chain_id !== '*' && chain_id !== null ? chain_id : '137' + + const currency = usd_amount ? FiatCurrency.USD : '*' + + for (const [key, value] of Object.entries(oldPolicy)) { + if (value === null || value === undefined || value === '*') { + continue + } + + if (key === 'user_id') { + res.when.push({ + criterion: Criterion.CHECK_PRINCIPAL_ID, + args: [value] + }) + } + + if (key === 'guild_user_role') { + const role = ['root', 'admin', 'member', 'manager'].includes(value) ? (value as UserRole) : UserRole.MEMBER + + res.when.push({ + criterion: Criterion.CHECK_PRINCIPAL_ROLE, + args: [role] + }) + } + + if (key === 'user_group') { + res.when.push({ + criterion: Criterion.CHECK_PRINCIPAL_GROUP, + args: [value] + }) + } + + if (key === 'chain_id') { + res.when.push({ + criterion: Criterion.CHECK_CHAIN_ID, + args: [value] + }) + } + + if (key === 'source_address') { + res.when.push({ + criterion: Criterion.CHECK_WALLET_ADDRESS, + args: [value] + }) + } + + if (key === 'destination_address') { + res.when.push({ + criterion: Criterion.CHECK_DESTINATION_ADDRESS, + args: [value] + }) + } + + if (key === 'contract_hex_signature') { + res.when.push({ + criterion: Criterion.CHECK_INTENT_HEX_SIGNATURE, + args: [value as Hex] + }) + } + + if (key === 'activity_type') { + let action = '' + let intent = '' + let token = '' + let contract = '' + let spender = '' + let tokenId = '' + + const when: PolicyCriterion[] = [] + + if (value === 'fungibleAssetTransfer') { + action = 'signTransaction' + if (assetType === 'erc20') { + intent = 'transferErc20' + token = `eip155:${chainId}/${assetType}:${assetAddress}` + } + if (assetType === 'native') { + intent = 'transferNative' + if (chainId === '1') { + token = `eip155:${chainId}/slip44:60` + } + if (chainId === '137') { + token = `eip155:${chainId}/slip44:966` + } + } + } + if (value === 'nftAssetTransfer') { + action = 'signTransaction' + if (assetType === 'erc721') { + intent = 'transferErc721' + } + if (assetType === 'erc1155') { + intent = 'transferERC1155' + } + if (assetAddress !== null && assetAddress !== '*') { + contract = `eip155:${chainId}/${assetAddress}` + if (assetType !== null && assetType !== '*' && assetTokenId !== null && assetTokenId !== '*') { + tokenId = `eip155:${chainId}/${assetType}:${assetAddress}/${assetTokenId}` + } + } + } + if (value === 'contractCall') { + action = 'signTransaction' + intent = 'callContract' + if (destination_account_type === 'contract' && destination_address !== '*') { + contract = `eip155:${chainId}/${destination_address}` + } + } + if (value === 'tokenApproval') { + action = 'signTransaction' + intent = 'approveTokenAllowance' + token = `eip155:${chainId}/${assetType || 'erc20'}:${assetAddress}` + if (destination_account_type === 'contract' && destination_address !== '*') { + spender = `eip155:${chainId}/${destination_address}` + } + } + if (value === 'signMessage') { + action = 'signMessage' + if (signing_type === 'personalSign') { + intent = 'signMessage' + } + if (signing_type === 'signTypedData') { + intent = 'signTypedData' + } + } + + if (action) { + when.push({ + criterion: Criterion.CHECK_ACTION, + args: [action] + } as PolicyCriterion) + } + if (intent) { + when.push({ + criterion: Criterion.CHECK_INTENT_TYPE, + args: [intent] + } as PolicyCriterion) + } + if (token) { + when.push({ + criterion: Criterion.CHECK_INTENT_TOKEN, + args: [token] + } as PolicyCriterion) + } + if (contract) { + when.push({ + criterion: Criterion.CHECK_INTENT_CONTRACT, + args: [contract] + } as PolicyCriterion) + } + if (spender) { + when.push({ + criterion: Criterion.CHECK_INTENT_SPENDER, + args: [spender] + } as PolicyCriterion) + } + if (tokenId) { + if (intent === 'transferErc721') { + when.push({ + criterion: Criterion.CHECK_ERC721_TOKEN_ID, + args: [tokenId] + } as PolicyCriterion) + } + if (intent === 'transferErc1155') { + when.push({ + criterion: Criterion.CHECK_ERC1155_TOKEN_ID, + args: [tokenId] + } as PolicyCriterion) + } + } + + res.when = res.when.concat(when) + } + + if (key === 'amount') { + let operator = ValueOperators.LESS_THAN + + if (comparison_operator === '>') { + operator = ValueOperators.GREATER_THAN + } else if (comparison_operator === '<') { + operator = ValueOperators.LESS_THAN + } else if (comparison_operator === '=') { + operator = ValueOperators.EQUAL + } + + res.when.push({ + criterion: Criterion.CHECK_INTENT_AMOUNT, + args: { currency, operator, value: `${value}` } + }) + } + + if (['domain_version', 'domain_name', 'domain_verifying_contract'].includes(key)) { + const args: SignTypedDataDomainCondition = {} + + if (domain_version !== null && domain_version !== '*') { + args['version'] = [domain_version] + } + + if (domain_name !== null && domain_name !== '*') { + args['name'] = [domain_name] + } + + if (domain_verifying_contract !== null && domain_verifying_contract !== '*') { + args['verifyingContract'] = [domain_verifying_contract as Address] + } + + if (Object.keys(args).length > 0) { + res.when.push({ + criterion: Criterion.CHECK_INTENT_DOMAIN, + args + }) + } + } + } + + if (res.then === Then.PERMIT) { + const approval: ApprovalCondition = { + approvalCount: 1, + countPrincipal: false, + approvalEntityType: EntityType.User, + entityIds: [] + } + + if (approval_threshold !== null && approval_threshold !== '*') { + approval.approvalCount = Number(approval_threshold) + } + if (approval_user_id !== null && approval_user_id !== '*') { + approval.approvalEntityType = EntityType.User + approval.entityIds = [approval_user_id] + } + if (approval_user_role !== null && approval_user_role !== '*') { + approval.approvalEntityType = EntityType.UserRole + approval.entityIds = [approval_user_role] + } + if (approval_user_group !== null && approval_user_group !== '*') { + approval.approvalEntityType = EntityType.UserGroup + approval.entityIds = [approval_user_group] + } + + if (approval.entityIds.length > 0) { + res.when.push({ + criterion: Criterion.CHECK_APPROVALS, + args: [approval] + }) + } + } + + return res +} + +const sendTranslatingRequest = async (data: { policies: OldPolicy[] }) => { + try { + console.log(data.policies.length) + + const res = await axios.post('http://localhost:3010/admin/policies', { + authentication: { + sig: '0x746ed2e4bf7311da76bc157c7fe8c0520b6e4c27ab96abf5a8d16fecbaac98b669418b2db9da8e6d3cbd4e1eaff1a9d9e765f0470e9b86c6694145778a8d46f81c', + alg: 'ES256K', + pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' + }, + approvals: [ + { + sig: '0xe86dffd265b7a76a9de0ee9078137271cbe32bb2bb8ee28a2935cc37f023193a51cd608701b9c40fc42be69eeb45c0bb375b5898828f1af4bf12e37ff1fe697f1c', + alg: 'ES256K', + pubKey: '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06' + }, + { + sig: '0xaffbddca4f16079f86a56d58f9ebb151c353e73c11a09791eb97f01ea0046c545ea0bd765ab1dc844ee0369f9123476b6f84b00b42b7ac1a16676b9a11e1a4031c', + alg: 'ES256K', + pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e' + } + ], + request: { + action: 'setPolicyRules', + nonce: 'random-nonce-111', + data: data.policies.map((policy) => { + const copy: OldPolicy = omit(policy, ['guild_id', 'sequence', 'version', 'amount']) + copy.amount = policy.amount !== null ? `${policy.amount}` : null + return translateLegacyPolicy(copy) + }) + } + }) + + console.log(res.data) + } catch (err) { + console.error(err.response.data) + } +} + +sendTranslatingRequest({ policies: [] as OldPolicy[] }) diff --git a/apps/authz/src/opa/template/meta-permissions.data.ts b/apps/authz/src/opa/template/meta-permissions.data.ts new file mode 100644 index 000000000..66b90e836 --- /dev/null +++ b/apps/authz/src/opa/template/meta-permissions.data.ts @@ -0,0 +1,63 @@ +import { Action, EntityType, UserRole } from '@narval/authz-shared' +import { Criterion, Policy, Then } from '../../shared/types/policy.type' + +const metaPermissions = [ + Action.CREATE_ORGANIZATION, + Action.CREATE_USER, + Action.UPDATE_USER, + Action.CREATE_CREDENTIAL, + Action.ASSIGN_USER_GROUP, + Action.ASSIGN_WALLET_GROUP, + Action.ASSIGN_USER_WALLET, + Action.DELETE_USER, + Action.REGISTER_WALLET, + Action.CREATE_ADDRESS_BOOK_ACCOUNT, + Action.EDIT_WALLET, + Action.UNASSIGN_WALLET, + Action.REGISTER_TOKENS, + Action.EDIT_USER_GROUP, + Action.DELETE_USER_GROUP, + Action.CREATE_WALLET_GROUP, + Action.DELETE_WALLET_GROUP +] + +export const permitMetaPermission: Policy = { + name: 'permitMetaPermission', + when: [ + { + criterion: Criterion.CHECK_ACTION, + args: metaPermissions + }, + { + criterion: Criterion.CHECK_PRINCIPAL_ROLE, + args: [UserRole.ADMIN] + }, + { + criterion: Criterion.CHECK_APPROVALS, + args: [ + { + approvalCount: 2, + countPrincipal: false, + approvalEntityType: EntityType.UserRole, + entityIds: [UserRole.ADMIN, UserRole.ROOT] + } + ] + } + ], + then: Then.PERMIT +} + +export const forbidMetaPermission: Policy = { + name: 'forbidMetaPermission', + when: [ + { + criterion: Criterion.CHECK_ACTION, + args: metaPermissions + }, + { + criterion: Criterion.CHECK_PRINCIPAL_ROLE, + args: [UserRole.ADMIN] + } + ], + then: Then.FORBID +} diff --git a/apps/authz/src/opa/template/mockData.ts b/apps/authz/src/opa/template/mockData.ts index 8dd0a8686..9d172c6d7 100644 --- a/apps/authz/src/opa/template/mockData.ts +++ b/apps/authz/src/opa/template/mockData.ts @@ -114,64 +114,3 @@ export const exampleForbidPolicy: Policy = { export const policies = { policies: [examplePermitPolicy, exampleForbidPolicy] } - -const metaPermissions = [ - Action.CREATE_ORGANIZATION, - Action.CREATE_USER, - Action.UPDATE_USER, - Action.CREATE_CREDENTIAL, - Action.ASSIGN_USER_GROUP, - Action.ASSIGN_WALLET_GROUP, - Action.ASSIGN_USER_WALLET, - Action.DELETE_USER, - Action.REGISTER_WALLET, - Action.CREATE_ADDRESS_BOOK_ACCOUNT, - Action.EDIT_WALLET, - Action.UNASSIGN_WALLET, - Action.REGISTER_TOKENS, - Action.EDIT_USER_GROUP, - Action.DELETE_USER_GROUP, - Action.CREATE_WALLET_GROUP, - Action.DELETE_WALLET_GROUP -] - -export const permitMetaPermission: Policy = { - name: 'permitMetaPermission', - when: [ - { - criterion: Criterion.CHECK_ACTION, - args: metaPermissions - }, - { - criterion: Criterion.CHECK_PRINCIPAL_ROLE, - args: [UserRole.ADMIN] - }, - { - criterion: Criterion.CHECK_APPROVALS, - args: [ - { - approvalCount: 2, - countPrincipal: false, - approvalEntityType: EntityType.UserRole, - entityIds: [UserRole.ADMIN, UserRole.ROOT] - } - ] - } - ], - then: Then.PERMIT -} - -export const forbidMetaPermission: Policy = { - name: 'forbidMetaPermission', - when: [ - { - criterion: Criterion.CHECK_ACTION, - args: metaPermissions - }, - { - criterion: Criterion.CHECK_PRINCIPAL_ROLE, - args: [UserRole.ADMIN] - } - ], - then: Then.FORBID -} diff --git a/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts b/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts index 362e04280..9587337be 100644 --- a/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts +++ b/apps/authz/src/shared/__test__/unit/opa.utils.spec.ts @@ -68,18 +68,35 @@ describe('criterionToString', () => { }) describe('reasonToString', () => { - it('returns reason for PERMIT rules', () => { + it('returns reason with approvals for PERMIT rules', () => { const item = { id: '12345', then: Then.PERMIT, name: 'policyName', - when: [] + when: [ + { + criterion: Criterion.CHECK_APPROVALS, + args: [{ approvalCount: 2, countPrincipal: false, approvalEntityType: EntityType.User, entityIds: [] }] + } + ] } expect(reasonToString(item)).toEqual( 'reason = {"type":"permit","policyId":"12345","policyName":"policyName","approvalsSatisfied":approvals.approvalsSatisfied,"approvalsMissing":approvals.approvalsMissing}' ) }) + it('returns reason without approvals for PERMIT rules', () => { + const item = { + id: '12345', + then: Then.PERMIT, + name: 'policyName', + when: [] + } + expect(reasonToString(item)).toEqual( + 'reason = {"type":"permit","policyId":"12345","policyName":"policyName","approvalsSatisfied":[],"approvalsMissing":[]}' + ) + }) + it('returns reason for FORBID rules', () => { const item = { id: '12345', diff --git a/apps/authz/src/shared/types/policy.type.ts b/apps/authz/src/shared/types/policy.type.ts index 5603dad61..2ea520005 100644 --- a/apps/authz/src/shared/types/policy.type.ts +++ b/apps/authz/src/shared/types/policy.type.ts @@ -52,7 +52,6 @@ export const Criterion = { CHECK_WALLET_ID: 'checkWalletId', CHECK_WALLET_ADDRESS: 'checkWalletAddress', CHECK_WALLET_ACCOUNT_TYPE: 'checkWalletAccountType', - CHECK_WALLET_CHAIN_ID: 'checkWalletChainId', CHECK_WALLET_GROUP: 'checkWalletGroup', CHECK_INTENT_TYPE: 'checkIntentType', CHECK_DESTINATION_ID: 'checkDestinationId', @@ -73,6 +72,7 @@ export const Criterion = { CHECK_INTENT_ALGORITHM: 'checkIntentAlgorithm', CHECK_INTENT_DOMAIN: 'checkIntentDomain', CHECK_PERMIT_DEADLINE: 'checkPermitDeadline', + CHECK_CHAIN_ID: 'checkChainId', CHECK_GAS_FEE_AMOUNT: 'checkGasFeeAmount', CHECK_NONCE_EXISTS: 'checkNonceExists', CHECK_NONCE_NOT_EXISTS: 'checkNonceNotExists', @@ -335,11 +335,12 @@ export class WalletAccountTypeCriterion extends BaseCriterion { args: AccountType[] } -export class WalletChainIdCriterion extends BaseCriterion { - @Matches(Criterion.CHECK_WALLET_CHAIN_ID) - criterion: typeof Criterion.CHECK_WALLET_CHAIN_ID +export class ChainIdCriterion extends BaseCriterion { + @Matches(Criterion.CHECK_CHAIN_ID) + criterion: typeof Criterion.CHECK_CHAIN_ID @IsNotEmptyArrayString() + @IsNumberString({}, { each: true }) args: string[] } @@ -574,7 +575,6 @@ const SUPPORTED_CRITERION = [ WalletIdCriterion, WalletAddressCriterion, WalletAccountTypeCriterion, - WalletChainIdCriterion, WalletGroupCriterion, IntentTypeCriterion, DestinationIdCriterion, @@ -595,6 +595,7 @@ const SUPPORTED_CRITERION = [ IntentAlgorithmCriterion, IntentDomainCriterion, PermitDeadlineCriterion, + ChainIdCriterion, GasFeeAmountCriterion, NonceRequiredCriterion, NonceNotRequiredCriterion, @@ -611,7 +612,6 @@ export type PolicyCriterion = | WalletIdCriterion | WalletAddressCriterion | WalletAccountTypeCriterion - | WalletChainIdCriterion | WalletGroupCriterion | IntentTypeCriterion | DestinationIdCriterion @@ -632,6 +632,7 @@ export type PolicyCriterion = | IntentAlgorithmCriterion | IntentDomainCriterion | PermitDeadlineCriterion + | ChainIdCriterion | GasFeeAmountCriterion | NonceRequiredCriterion | NonceNotRequiredCriterion @@ -682,8 +683,6 @@ const instantiateCriterion = (criterion: PolicyCriterion) => { return plainToInstance(WalletAddressCriterion, criterion) case Criterion.CHECK_WALLET_ACCOUNT_TYPE: return plainToInstance(WalletAccountTypeCriterion, criterion) - case Criterion.CHECK_WALLET_CHAIN_ID: - return plainToInstance(WalletChainIdCriterion, criterion) case Criterion.CHECK_WALLET_GROUP: return plainToInstance(WalletGroupCriterion, criterion) case Criterion.CHECK_INTENT_TYPE: @@ -724,6 +723,8 @@ const instantiateCriterion = (criterion: PolicyCriterion) => { return plainToInstance(IntentDomainCriterion, criterion) case Criterion.CHECK_PERMIT_DEADLINE: return plainToInstance(PermitDeadlineCriterion, criterion) + case Criterion.CHECK_CHAIN_ID: + return plainToInstance(ChainIdCriterion, criterion) case Criterion.CHECK_GAS_FEE_AMOUNT: return plainToInstance(GasFeeAmountCriterion, criterion) case Criterion.CHECK_NONCE_EXISTS: diff --git a/apps/authz/src/shared/utils/opa.utils.ts b/apps/authz/src/shared/utils/opa.utils.ts index 2b5a5f680..95405a72e 100644 --- a/apps/authz/src/shared/utils/opa.utils.ts +++ b/apps/authz/src/shared/utils/opa.utils.ts @@ -26,13 +26,20 @@ export const criterionToString = (item: PolicyCriterion) => { export const reasonToString = (item: Policy & { id: string }) => { if (item.then === Then.PERMIT) { + const approvals = item.when.find((c) => c.criterion === Criterion.CHECK_APPROVALS) + const approvalsSatisfied = approvals + ? '"approvalsSatisfied":approvals.approvalsSatisfied' + : '"approvalsSatisfied":[]' + const approvalsMissing = approvals ? '"approvalsMissing":approvals.approvalsMissing' : '"approvalsMissing":[]' + const reason = [ `"type":"${item.then}"`, `"policyId":"${item.id}"`, `"policyName":"${item.name}"`, - '"approvalsSatisfied":approvals.approvalsSatisfied', - '"approvalsMissing":approvals.approvalsMissing' + approvalsSatisfied, + approvalsMissing ] + return `reason = {${reason.join(',')}}` }