1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-06 04:00:05 +00:00

fix(*): store CSRF tokens in Request Client

This commit is contained in:
Krishna Acondy
2021-01-24 18:23:18 +00:00
parent 3a9cd46e6e
commit e0d85f458b
7 changed files with 572 additions and 679 deletions

View File

@@ -6,7 +6,6 @@ import {
Context,
ContextAllAttributes,
Folder,
CsrfToken,
EditContextInput,
JobDefinition,
PollOptions
@@ -16,35 +15,34 @@ import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager'
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger'
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { parseAndSubmitAuthorizeForm } from './auth'
import { RequestClient } from './request/client'
/**
* A client for interfacing with the SAS Viya REST API.
*
*/
export class SASViyaApiClient {
private httpClient: AxiosInstance
constructor(
private serverUrl: string,
private rootFolderName: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void
private requestClient: RequestClient
) {
if (serverUrl) isUrl(serverUrl)
this.httpClient = axios.create({ baseURL: serverUrl })
}
private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
this.setCsrfToken
this.requestClient
)
private contextManager = new ContextManager(
this.serverUrl,
this.requestClient
)
private contextManager = new ContextManager(this.serverUrl, this.setCsrfToken)
private folderMap = new Map<string, Job[]>()
public get debug() {
@@ -143,10 +141,9 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.get<{ items: Context[] }>(
`/compute/contexts?limit=10000`,
accessToken
)
const { result: contexts } = await this.requestClient.get<{
items: Context[]
}>(`/compute/contexts?limit=10000`, accessToken)
const executionContext =
contexts.items && contexts.items.length
@@ -163,7 +160,7 @@ export class SASViyaApiClient {
'Content-Type': 'application/json'
}
}
const { result: createdSession } = await this.post<Session>(
const { result: createdSession } = await this.requestClient.post<Session>(
`/compute/contexts/${executionContext.id}/sessions`,
{},
accessToken
@@ -376,13 +373,15 @@ export class SASViyaApiClient {
variables: jobVariables,
arguments: jobArguments
})
const { result: postedJob, etag } = await this.post<Job>(
`/compute/sessions/${executionSessionId}/jobs`,
jobRequestBody,
accessToken
).catch((err: any) => {
throw err
})
const { result: postedJob, etag } = await this.requestClient
.post<Job>(
`/compute/sessions/${executionSessionId}/jobs`,
jobRequestBody,
accessToken
)
.catch((err: any) => {
throw err
})
if (!waitForResult) {
return session
@@ -404,12 +403,14 @@ export class SASViyaApiClient {
pollOptions
)
const { result: currentJob } = await this.get<Job>(
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
accessToken
).catch((err) => {
throw err
})
const { result: currentJob } = await this.requestClient
.get<Job>(
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
accessToken
)
.catch((err) => {
throw err
})
let jobResult
let log
@@ -417,10 +418,8 @@ export class SASViyaApiClient {
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) {
log = await this.get<any>(
`${logLink.href}/content?limit=10000`,
accessToken
)
log = await this.requestClient
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
@@ -442,34 +441,30 @@ export class SASViyaApiClient {
}
if (resultLink) {
jobResult = await this.get<any>(
resultLink,
accessToken,
'text/plain'
).catch(async (e) => {
if (e && e.status === 404) {
if (logLink) {
log = await this.get<any>(
`${logLink.href}/content?limit=10000`,
accessToken
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
jobResult = await this.requestClient
.get<any>(resultLink, accessToken, 'text/plain')
.catch(async (e) => {
if (e && e.status === 404) {
if (logLink) {
log = await this.requestClient
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
return Promise.reject({
status: 500,
log
})
return Promise.reject({
status: 500,
log
})
}
}
}
return {
result: JSON.stringify(e)
}
})
return {
result: JSON.stringify(e)
}
})
}
await this.sessionManager
@@ -559,7 +554,9 @@ export class SASViyaApiClient {
}
}
const { result: createFolderResponse } = await this.post<Folder>(
const {
result: createFolderResponse
} = await this.requestClient.post<Folder>(
`/folders/folders?parentFolderUri=${parentFolderUri}`,
JSON.stringify({
name: folderName,
@@ -599,7 +596,7 @@ export class SASViyaApiClient {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
}
return await this.post<Job>(
return await this.requestClient.post<Job>(
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
JSON.stringify({
name: jobName,
@@ -763,7 +760,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const deleteResponse = await this.delete(url, accessToken)
const deleteResponse = await this.requestClient.delete(url, accessToken)
return deleteResponse.result
}
@@ -835,7 +832,9 @@ export class SASViyaApiClient {
throw new Error(`URI of job definition was not found.`)
}
const { result: jobDefinition } = await this.get<JobDefinition>(
const {
result: jobDefinition
} = await this.requestClient.get<JobDefinition>(
`${this.serverUrl}${jobDefinitionLink.href}`,
accessToken
)
@@ -913,7 +912,7 @@ export class SASViyaApiClient {
(l) => l.rel === 'getResource'
)?.href
const { result: jobDefinition } = await this.get<Job>(
const { result: jobDefinition } = await this.requestClient.get<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
accessToken
)
@@ -948,12 +947,13 @@ export class SASViyaApiClient {
jobDefinition,
arguments: jobArguments
})
const { result: postedJob, etag } = await this.post<Job>(
const { result: postedJob, etag } = await this.requestClient.post<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequestBody
postJobRequestBody,
accessToken
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.get<Job>(
const { result: currentJob } = await this.requestClient.get<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
accessToken
)
@@ -964,17 +964,16 @@ export class SASViyaApiClient {
const resultLink = currentJob.results['_webout.json']
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (resultLink) {
jobResult = await this.get<any>(
jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`,
accessToken,
'text/plain'
)
}
if (debug && logLink) {
log = await this.get<any>(
`${this.serverUrl}${logLink.href}/content`,
accessToken
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
log = await this.requestClient
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
}
if (jobStatus === 'failed') {
return Promise.reject({ error: currentJob.error, log })
@@ -991,11 +990,14 @@ export class SASViyaApiClient {
}
const url = '/folders/folders/@item?path=' + path
const { result: folder } = await this.get<Folder>(`${url}`, accessToken)
const { result: folder } = await this.requestClient.get<Folder>(
`${url}`,
accessToken
)
if (!folder) {
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
}
const { result: members } = await this.get<{ items: any[] }>(
const { result: members } = await this.requestClient.get<{ items: any[] }>(
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
accessToken
)
@@ -1033,7 +1035,7 @@ export class SASViyaApiClient {
Promise.reject(`Job state link was not found.`)
}
const { result: state } = await this.get<string>(
const { result: state } = await this.requestClient.get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
accessToken,
'text/plain'
@@ -1054,7 +1056,7 @@ export class SASViyaApiClient {
postedJobState === 'pending'
) {
if (stateLink) {
const { result: jobState } = await this.get<string>(
const { result: jobState } = await this.requestClient.get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
accessToken,
'text/plain'
@@ -1100,7 +1102,7 @@ export class SASViyaApiClient {
)
}
const uploadResponse = await this.uploadFile(
const uploadResponse = await this.requestClient.uploadFile(
`${this.serverUrl}/files/files#rawUpload`,
csv,
accessToken
@@ -1113,12 +1115,11 @@ export class SASViyaApiClient {
private async getFolderUri(folderPath: string, accessToken?: string) {
const url = '/folders/folders/@item?path=' + folderPath
const { result: folder } = await this.get<Folder>(
`${this.serverUrl}${url}`,
accessToken
).catch(() => {
return { result: null }
})
const { result: folder } = await this.requestClient
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
.catch(() => {
return { result: null }
})
if (!folder) return undefined
return `/folders/folders/${folder.id}`
@@ -1127,12 +1128,11 @@ export class SASViyaApiClient {
private async getRecycleBinUri(accessToken: string) {
const url = '/folders/folders/@myRecycleBin'
const { result: folder } = await this.get<Folder>(
`${this.serverUrl}${url}`,
accessToken
).catch(() => {
return { result: null }
})
const { result: folder } = await this.requestClient
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
.catch(() => {
return { result: null }
})
if (!folder) return undefined
@@ -1196,27 +1196,31 @@ export class SASViyaApiClient {
const sourceFolderId = sourceFolderUri?.split('/').pop()
const url = sourceFolderUri
const { result: folder } = await this.patch<Folder>(
`${this.serverUrl}${url}`,
JSON.stringify({
id: sourceFolderId,
name: targetFolderName,
parentFolderUri: targetParentFolderUri
}),
accessToken
).catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
})
const { result: folder } = await this.requestClient
.patch<Folder>(
`${this.serverUrl}${url}`,
JSON.stringify({
id: sourceFolderId,
name: targetFolderName,
parentFolderUri: targetParentFolderUri
}),
accessToken
)
.catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder
.split('/')
.pop()}' was not found.`
})
}
throw notFoundError
}
throw notFoundError
}
throw err
})
throw err
})
if (!folder) return undefined
@@ -1244,175 +1248,4 @@ export class SASViyaApiClient {
return movedFolder
}
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
this.csrfToken = csrfToken
this.setCsrfToken(csrfToken)
}
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
this.fileUploadCsrfToken = csrfToken
}
private get<T>(
url: string,
accessToken?: string,
contentType = 'application/json'
) {
const headers: any = {
'Content-Type': contentType
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const requestConfig: AxiosRequestConfig = {
headers,
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.headers.Accept = '*/*'
requestConfig.transformResponse = undefined
}
return this.httpClient.get<T>(url, requestConfig).then((response) => ({
result: response.data,
etag: response.headers['etag']
}))
}
private post<T>(
url: string,
data: any = {},
accessToken?: string
): Promise<{ result: T; etag: string }> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
})
.catch((e) => {
const response = e.response as AxiosResponse
if (response.status === 403 || response.status === 449) {
const tokenHeader = (response.headers[
'x-csrf-header'
] as string)?.toLowerCase()
if (tokenHeader) {
const token = response.headers[tokenHeader]
this.setCsrfTokenLocal({
headerName: tokenHeader,
value: token || ''
})
return this.post<T>(url, data, accessToken)
}
throw e
}
throw e
})
}
private delete<T>(url: string, accessToken?: string) {
const headers: any = {}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
const requestConfig: AxiosRequestConfig = {
headers
}
return this.httpClient.delete<T>(url, requestConfig).then((response) => ({
result: response.data,
etag: response.headers['etag']
}))
}
private patch<T>(url: string, data: any = {}, accessToken?: string) {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.csrfToken?.value) {
headers[this.csrfToken.headerName] = this.csrfToken.value
}
return this.httpClient
.patch<T>(url, data, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data as T,
etag: response.headers['etag'] as string
}
})
}
private uploadFile(
url: string,
content: string,
accessToken?: string
): Promise<any> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
if (this.fileUploadCsrfToken?.value) {
headers[
this.fileUploadCsrfToken.headerName
] = this.fileUploadCsrfToken.value
}
return this.httpClient
.post(url, content, { headers, withCredentials: true })
.then((response) => {
return {
result: response.data,
etag: response.headers['etag'] as string
}
})
.catch((e) => {
const response = e.response as AxiosResponse
if (response.status === 403 || response.status === 449) {
const tokenHeader = (response.headers[
'x-csrf-header'
] as string)?.toLowerCase()
if (tokenHeader) {
const token = response.headers[tokenHeader]
this.setFileUploadCsrfToken({
headerName: tokenHeader,
value: token || ''
})
return this.uploadFile(url, content, accessToken)
}
throw e
}
throw e
})
}
}