Skip to content

Commit 471b377

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oracle-epm-tax-reporting): guard upload directories and clarify status inputs
1 parent 0228944 commit 471b377

10 files changed

Lines changed: 99 additions & 26 deletions

File tree

apps/docs/content/docs/integrations/oracle_epm_tax_reporting.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ Inspect a submitted planning or supplemental job using its correct route family.
364364
| Parameter | Type | Required | Description |
365365
| --------- | ---- | -------- | ----------- |
366366
| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential |
367-
| `application` | string | No | Exact Tax Reporting application name; use application discovery or a tenant-specific reference. |
367+
| `application` | string | No | Exact Tax Reporting application name. Required for planning \(default\) and supplemental_collection jobs; omit only for supplemental_dimension. |
368368
| `jobId` | string | Yes | Submitted Oracle job instance ID, not a job definition name. |
369369
| `jobFamily` | string | No | planning \(default\), supplemental_collection \(fcmjobs\), or supplemental_dimension \(sdm/jobs\). Use the family that submitted the job. |
370370
| `waitForCompletion` | boolean | No | Wait at most 120 seconds, subject to the execution deadline. Default false. Timeout or local cancellation does not cancel the Oracle job; check its status before resubmitting. |
@@ -672,7 +672,7 @@ Inspect a report job using the documented standalone route or the distinct route
672672
| --------- | ---- | -------- | ----------- |
673673
| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential |
674674
| `jobId` | string | Yes | Submitted Oracle job instance ID, not a job definition name. |
675-
| `module` | string | No | Report generation: FCM \(Task Manager\) or SDM. Report status: FCCS \(Task Manager\) or SDM. |
675+
| `module` | string | No | FCCS \(Task Manager\) or SDM. Required for standalone \(default\) and generated_report status routes; omit only for user_details. |
676676
| `reportStatusRoute` | string | No | standalone \(default\): documented /arm job endpoint; generated_report: route used by Generate Report Job Status links; user_details: route used by User Details Job Status links. They are not interchangeable. |
677677
| `waitForCompletion` | boolean | No | Wait at most 120 seconds, subject to the execution deadline. Default false. Timeout or local cancellation does not cancel the Oracle job; check its status before resubmitting. |
678678
| `downloadReport` | boolean | No | Store the completed report as a Sim file using its validated report-content link \(default false\). Requires a workflow execution context. |

apps/sim/lib/internal/oracle-epm-tax-reporting/client.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,15 @@ describe('Tax Reporting route contracts', () => {
114114
).rejects.toThrow()
115115
expect(mocks.fetch).toHaveBeenCalledTimes(1)
116116
})
117+
118+
it('rejects upload directory traversal at the declared endpoint boundary', async () => {
119+
await expect(
120+
createTaxReportingClient(auth).request(taxEndpoints.upload_file, {
121+
pathParams: { fileName: 'tax.csv' },
122+
query: { extDirPath: 'inbox/../outbox' },
123+
stream: Buffer.from('tax'),
124+
})
125+
).rejects.toThrow()
126+
expect(mocks.fetch).not.toHaveBeenCalled()
127+
})
117128
})

apps/sim/lib/internal/oracle-epm-tax-reporting/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
OracleEpmPathPart,
1010
OracleEpmRouteSpace,
1111
} from '@/lib/internal/oracle-epm/types'
12+
import { TAX_UPLOAD_DIRECTORY_PATTERN } from '@/tools/oracle_epm_tax_reporting/utils'
1213

1314
/** Product-owned limits; requests never retry a potentially accepted mutation. */
1415
export const TAX_JSON_BYTES = 2 * 1024 * 1024
@@ -125,7 +126,7 @@ export const taxEndpoints = {
125126
body: 'stream',
126127
maxRequestBytes: TAX_UPLOAD_BYTES,
127128
query: {
128-
extDirPath: { kind: 'string', maxBytes: 255, pattern: /^(inbox|outbox)(\/[^/\\]+)*$/ },
129+
extDirPath: { kind: 'string', maxBytes: 255, pattern: TAX_UPLOAD_DIRECTORY_PATTERN },
129130
},
130131
}),
131132
download_file: endpoint(files, repositoryFile, {

apps/sim/lib/internal/oracle-epm-tax-reporting/operations.test.ts

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -435,22 +435,50 @@ describe('Tax Reporting provider behavior', () => {
435435
}
436436
)
437437

438-
it('rejects an unsafe report status link before a second authenticated request', async () => {
439-
mocks.fetch.mockResolvedValueOnce(
440-
Response.json({
441-
status: -1,
442-
links: [
443-
{
444-
rel: 'Job Status',
445-
href: 'https://other.example.com/HyperionPlanning/rest/fcmapi/v1/fcm/job/224',
446-
action: 'GET',
447-
},
448-
],
449-
})
450-
)
438+
it.each([false, true])(
439+
'rejects an unsafe report status link without another request when waiting is %s',
440+
async (waitForCompletion) => {
441+
mocks.fetch.mockResolvedValueOnce(
442+
Response.json({
443+
status: -1,
444+
links: [
445+
{
446+
rel: 'Job Status',
447+
href: 'https://other.example.com/HyperionPlanning/rest/fcmapi/v1/fcm/job/224',
448+
action: 'GET',
449+
},
450+
],
451+
})
452+
)
453+
await expect(
454+
execute('generate_user_details_report', { fileName: 'users.csv', waitForCompletion })
455+
).rejects.toThrow()
456+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
457+
}
458+
)
459+
460+
it('returns a trackable no-wait report snapshot without polling or resubmitting', async () => {
461+
const report = {
462+
status: -1,
463+
details: 'In Process',
464+
links: [
465+
{
466+
rel: 'Job Status',
467+
href: auth.instanceUrl + '/HyperionPlanning/rest/fcmapi/v1/fcm/job/224',
468+
action: 'GET',
469+
},
470+
],
471+
}
472+
mocks.fetch.mockResolvedValueOnce(Response.json(report))
473+
expect(await execute('generate_user_details_report', { fileName: 'users.csv' })).toEqual(report)
474+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
475+
})
476+
477+
it('reports an untrackable pending submission without retrying even when not waiting', async () => {
478+
mocks.fetch.mockResolvedValueOnce(Response.json({ status: -1 }))
451479
await expect(
452-
execute('generate_user_details_report', { fileName: 'users.csv', waitForCompletion: true })
453-
).rejects.toThrow()
480+
execute('generate_user_details_report', { fileName: 'users.csv' })
481+
).rejects.toThrow('do not resubmit automatically')
454482
expect(mocks.fetch).toHaveBeenCalledTimes(1)
455483
})
456484

@@ -460,16 +488,31 @@ describe('Tax Reporting provider behavior', () => {
460488
'not found'
461489
)
462490
expect(mocks.fetch).not.toHaveBeenCalled()
463-
await execute('upload_file', { file: sourceFile, fileName: 'tax.csv', directory: 'inbox' })
491+
await execute('upload_file', {
492+
file: sourceFile,
493+
fileName: 'tax.csv',
494+
directory: 'inbox/Tax data/v1.2',
495+
})
464496
expect(mocks.open).toHaveBeenLastCalledWith(
465497
expect.objectContaining({ userId: 'trusted-user', maxBytes: 10 * 1024 * 1024 })
466498
)
467499
expect(mocks.fetch.mock.calls[0][2].body).toEqual(Buffer.from('tax'))
468500
expect(mocks.fetch.mock.calls[0][0]).toContain(
469-
'applicationsnapshots/tax.csv/contents?extDirPath=inbox'
501+
'applicationsnapshots/tax.csv/contents?extDirPath=inbox%2FTax+data%2Fv1.2'
470502
)
471503
})
472504

505+
it.each(['inbox/../outbox', 'outbox/.', 'inbox/nested/..', 'inbox/./nested'])(
506+
'rejects upload directory %s before reading the source or submitting a mutation',
507+
(directory) => {
508+
expect(() =>
509+
execute('upload_file', { file: sourceFile, fileName: 'tax.csv', directory })
510+
).toThrow()
511+
expect(mocks.open).not.toHaveBeenCalled()
512+
expect(mocks.fetch).not.toHaveBeenCalled()
513+
}
514+
)
515+
473516
it('stores downloads as canonical files and rejects Oracle JSON error bodies', async () => {
474517
mocks.fetch.mockResolvedValueOnce(
475518
new Response('report', { headers: { 'content-type': 'text/csv' } })

apps/sim/lib/internal/oracle-epm-tax-reporting/operations.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ function getReportLink(
191191
) {
192192
const candidates = job.links?.filter((link) => link.rel === 'Job Status') ?? []
193193
if (candidates.length !== 1)
194-
throw new TaxReportingContractError('Tax Reporting did not return exactly one Job Status link')
194+
throw new TaxReportingContractError(
195+
'Tax Reporting accepted a pending report without exactly one Job Status link; do not resubmit automatically'
196+
)
195197
const link = candidates[0]
196198
return client.validateReturnedLink(policy, {
197199
rel: 'Job Status',

apps/sim/lib/internal/oracle-epm-tax-reporting/schema.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { z } from 'zod'
22
import { userFileSchema } from '@/lib/api/contracts/primitives'
3-
import { assertTaxInputBudget } from '@/tools/oracle_epm_tax_reporting/utils'
3+
import {
4+
assertTaxInputBudget,
5+
TAX_UPLOAD_DIRECTORY_PATTERN,
6+
} from '@/tools/oracle_epm_tax_reporting/utils'
47

58
const name = z.string().min(1).max(255)
69
const text = z.string().max(16384)
@@ -181,7 +184,7 @@ const operationShapes = {
181184
upload_file: {
182185
file: userFileSchema,
183186
fileName: name,
184-
directory: name.regex(/^(inbox|outbox)(\/[^/\\]+)*$/).optional(),
187+
directory: name.regex(TAX_UPLOAD_DIRECTORY_PATTERN).optional(),
185188
},
186189
download_file: { fileName: name },
187190
} as const

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/oracle_epm_tax_reporting/get_job_status.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export const oracleEpmTaxReportingGetJobStatusTool: InternalToolConfig<
2020
oauth: taxOAuth,
2121
params: {
2222
...taxAuthParams,
23-
application: { ...taxFields.application, required: false },
23+
application: {
24+
...taxFields.application,
25+
required: false,
26+
description:
27+
'Exact Tax Reporting application name. Required for planning (default) and supplemental_collection jobs; omit only for supplemental_dimension.',
28+
},
2429
jobId: taxFields.jobId,
2530
jobFamily: taxFields.jobFamily,
2631
waitForCompletion: taxFields.waitForCompletion,

apps/sim/tools/oracle_epm_tax_reporting/get_report_status.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ export const oracleEpmTaxReportingGetReportStatusTool: InternalToolConfig<
2121
params: {
2222
...taxAuthParams,
2323
jobId: taxFields.jobId,
24-
module: { ...taxFields.module, required: false },
24+
module: {
25+
...taxFields.module,
26+
required: false,
27+
description:
28+
'FCCS (Task Manager) or SDM. Required for standalone (default) and generated_report status routes; omit only for user_details.',
29+
},
2530
reportStatusRoute: taxFields.reportStatusRoute,
2631
waitForCompletion: taxFields.waitForCompletion,
2732
downloadReport: taxFields.downloadReport,

apps/sim/tools/oracle_epm_tax_reporting/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { OAuthConfig, ToolConfig } from '@/tools/types'
22

3+
/** Raw Oracle upload directories; never normalize caller-supplied dot segments. */
4+
export const TAX_UPLOAD_DIRECTORY_PATTERN = /^(inbox|outbox)(\/(?!\.{1,2}(?:\/|$))[^/\\]+)*$/
5+
36
export const taxOAuth = {
47
required: true,
58
provider: 'oracle_epm_tax_reporting',

0 commit comments

Comments
 (0)