diff --git a/DSL/Resql/training/POST/use-nonce.sql b/DSL/Resql/training/POST/use-nonce.sql new file mode 100644 index 000000000..52fd662e0 --- /dev/null +++ b/DSL/Resql/training/POST/use-nonce.sql @@ -0,0 +1 @@ +update request_nonces set used_at = now() where nonce = :updated_nonce and used_at is null returning nonce; diff --git a/DSL/Ruuter/services/GET/get-new-nonce.yml b/DSL/Ruuter/services/GET/get-new-nonce.yml new file mode 100644 index 000000000..b37fb6a2f --- /dev/null +++ b/DSL/Ruuter/services/GET/get-new-nonce.yml @@ -0,0 +1,19 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'GET-NEW-NONCE'" + method: get + accepts: json + returns: json + namespace: service + +get_new_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/get-new-nonce" + result: nonce_res + +return_result: + wrapper: false + return: ${nonce_res.response.body[0].nonce} + next: end diff --git a/DSL/Ruuter/services/GET/services/active/.guard b/DSL/Ruuter/services/GET/services/active/.guard index 644353775..9e768b6bc 100644 --- a/DSL/Ruuter/services/GET/services/active/.guard +++ b/DSL/Ruuter/services/GET/services/active/.guard @@ -1,4 +1,61 @@ -guard_allow_all: +process_request: + switch: + - condition: ${ incoming.headers['x-ruuter-nonce'] != null } + next: verify_header_nonce + - condition: ${ incoming.params['ruuter-nonce'] != null } + next: verify_param_nonce + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +verify_header_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.headers['x-ruuter-nonce']} + result: nonce_response + next: check_inline + +verify_param_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.params['ruuter-nonce']} + result: nonce_response + next: check_inline + +check_inline: + inline: extAuth + result: extauth + +check_nonce: + switch: + - condition: ${ !extauth || (nonce_response.response.body[0] == null) } + next: guard_fail + next: guard_success + +authenticate: + template: "[#SERVICE_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: return: "success" status: 200 next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter/services/GET/services/draft/.guard b/DSL/Ruuter/services/GET/services/draft/.guard index 644353775..9e768b6bc 100644 --- a/DSL/Ruuter/services/GET/services/draft/.guard +++ b/DSL/Ruuter/services/GET/services/draft/.guard @@ -1,4 +1,61 @@ -guard_allow_all: +process_request: + switch: + - condition: ${ incoming.headers['x-ruuter-nonce'] != null } + next: verify_header_nonce + - condition: ${ incoming.params['ruuter-nonce'] != null } + next: verify_param_nonce + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +verify_header_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.headers['x-ruuter-nonce']} + result: nonce_response + next: check_inline + +verify_param_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.params['ruuter-nonce']} + result: nonce_response + next: check_inline + +check_inline: + inline: extAuth + result: extauth + +check_nonce: + switch: + - condition: ${ !extauth || (nonce_response.response.body[0] == null) } + next: guard_fail + next: guard_success + +authenticate: + template: "[#SERVICE_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: return: "success" status: 200 next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter/services/GET/services/inactive/.guard b/DSL/Ruuter/services/GET/services/inactive/.guard index 644353775..9e768b6bc 100644 --- a/DSL/Ruuter/services/GET/services/inactive/.guard +++ b/DSL/Ruuter/services/GET/services/inactive/.guard @@ -1,4 +1,61 @@ -guard_allow_all: +process_request: + switch: + - condition: ${ incoming.headers['x-ruuter-nonce'] != null } + next: verify_header_nonce + - condition: ${ incoming.params['ruuter-nonce'] != null } + next: verify_param_nonce + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +verify_header_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.headers['x-ruuter-nonce']} + result: nonce_response + next: check_inline + +verify_param_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.params['ruuter-nonce']} + result: nonce_response + next: check_inline + +check_inline: + inline: extAuth + result: extauth + +check_nonce: + switch: + - condition: ${ !extauth || (nonce_response.response.body[0] == null) } + next: guard_fail + next: guard_success + +authenticate: + template: "[#SERVICE_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: return: "success" status: 200 next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter/services/POST/services/active/.guard b/DSL/Ruuter/services/POST/services/active/.guard index 644353775..9e768b6bc 100644 --- a/DSL/Ruuter/services/POST/services/active/.guard +++ b/DSL/Ruuter/services/POST/services/active/.guard @@ -1,4 +1,61 @@ -guard_allow_all: +process_request: + switch: + - condition: ${ incoming.headers['x-ruuter-nonce'] != null } + next: verify_header_nonce + - condition: ${ incoming.params['ruuter-nonce'] != null } + next: verify_param_nonce + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +verify_header_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.headers['x-ruuter-nonce']} + result: nonce_response + next: check_inline + +verify_param_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.params['ruuter-nonce']} + result: nonce_response + next: check_inline + +check_inline: + inline: extAuth + result: extauth + +check_nonce: + switch: + - condition: ${ !extauth || (nonce_response.response.body[0] == null) } + next: guard_fail + next: guard_success + +authenticate: + template: "[#SERVICE_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: return: "success" status: 200 next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter/services/POST/services/draft/.guard b/DSL/Ruuter/services/POST/services/draft/.guard index 644353775..9e768b6bc 100644 --- a/DSL/Ruuter/services/POST/services/draft/.guard +++ b/DSL/Ruuter/services/POST/services/draft/.guard @@ -1,4 +1,61 @@ -guard_allow_all: +process_request: + switch: + - condition: ${ incoming.headers['x-ruuter-nonce'] != null } + next: verify_header_nonce + - condition: ${ incoming.params['ruuter-nonce'] != null } + next: verify_param_nonce + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +verify_header_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.headers['x-ruuter-nonce']} + result: nonce_response + next: check_inline + +verify_param_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.params['ruuter-nonce']} + result: nonce_response + next: check_inline + +check_inline: + inline: extAuth + result: extauth + +check_nonce: + switch: + - condition: ${ !extauth || (nonce_response.response.body[0] == null) } + next: guard_fail + next: guard_success + +authenticate: + template: "[#SERVICE_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: return: "success" status: 200 next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter/services/POST/services/inactive/.guard b/DSL/Ruuter/services/POST/services/inactive/.guard index 644353775..9e768b6bc 100644 --- a/DSL/Ruuter/services/POST/services/inactive/.guard +++ b/DSL/Ruuter/services/POST/services/inactive/.guard @@ -1,4 +1,61 @@ -guard_allow_all: +process_request: + switch: + - condition: ${ incoming.headers['x-ruuter-nonce'] != null } + next: verify_header_nonce + - condition: ${ incoming.params['ruuter-nonce'] != null } + next: verify_param_nonce + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +verify_header_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.headers['x-ruuter-nonce']} + result: nonce_response + next: check_inline + +verify_param_nonce: + call: http.post + args: + url: "[#SERVICE_TRAINING_RESQL]/use-nonce" + body: + updated_nonce: ${incoming.params['ruuter-nonce']} + result: nonce_response + next: check_inline + +check_inline: + inline: extAuth + result: extauth + +check_nonce: + switch: + - condition: ${ !extauth || (nonce_response.response.body[0] == null) } + next: guard_fail + next: guard_success + +authenticate: + template: "[#SERVICE_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: return: "success" status: 200 next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/GUI/src/resources/api-constants.ts b/GUI/src/resources/api-constants.ts index a6c2af4d9..6c0b58589 100644 --- a/GUI/src/resources/api-constants.ts +++ b/GUI/src/resources/api-constants.ts @@ -16,6 +16,7 @@ export const changeServiceStatus = (): string => `${baseUrl}/services/status`; export const createNewService = (): string => `${baseUrl}/services/add`; export const testService = (state: ServiceState, serviceName: string): string => `${baseUrl}/services/${state.toLowerCase()}/${serviceName}`; +export const getNewNonce = (): string => `${baseUrl}/get-new-nonce`; export const editService = (id: string): string => `${baseUrl}/services/edit?id=${id}`; export const getServicesList = (): string => `${baseUrl}/services`; export const getActiveServicesList = (): string => `${baseUrl}/active-services`; diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts index e8cdb07bb..7ddc688ab 100644 --- a/GUI/src/services/service-builder.ts +++ b/GUI/src/services/service-builder.ts @@ -486,6 +486,13 @@ export function getYamlContent( returns: 'json', namespace: 'service', allowlist: { + header: [ + { + field: 'x-ruuter-nonce', + type: 'string', + description: 'The nonce for the request', + }, + ], body: [ { field: 'chatId', diff --git a/GUI/src/services/service-tester.test.ts b/GUI/src/services/service-tester.test.ts index 4499b4927..1cd937f3f 100644 --- a/GUI/src/services/service-tester.test.ts +++ b/GUI/src/services/service-tester.test.ts @@ -9,6 +9,7 @@ import { clearPreviousTestStates, executeService, executeServiceTest, + fetchNonce, getInvalidNodes, handleTestError, hasResponseData, @@ -28,12 +29,14 @@ vi.mock('i18next', () => ({ vi.mock('./api', () => ({ default: { post: vi.fn(), + get: vi.fn(), }, createApiInstance: vi.fn(), })); vi.mock('resources/api-constants', () => ({ testService: vi.fn(), + getNewNonce: vi.fn(), })); // Mock dependencies for handleTestError @@ -715,11 +718,13 @@ describe('executeServiceTest', () => { const state = ServiceState.Active; const name = 'test-service'; const input = 'test-input'; + const nonce = 'test-nonce'; - await executeServiceTest(headerValue, state, name, input); + await executeServiceTest(headerValue, state, name, input, nonce); expect(mockCreateApiInstance).toHaveBeenCalledWith({ 'x-ruuter-testing': headerValue, + 'x-ruuter-nonce': nonce, }); }); @@ -728,8 +733,9 @@ describe('executeServiceTest', () => { const state = ServiceState.Active; const name = 'test-service'; const input = 'test-input'; + const nonce = 'test-nonce'; - await executeServiceTest(headerValue, state, name, input); + await executeServiceTest(headerValue, state, name, input, nonce); expect(mockTestService).toHaveBeenCalledWith(state, name); }); @@ -739,9 +745,10 @@ describe('executeServiceTest', () => { const state = ServiceState.Active; const name = 'test-service'; const input = 'test-input'; + const nonce = 'test-nonce'; const expectedEndpoint = '/test-endpoint'; - await executeServiceTest(headerValue, state, name, input); + await executeServiceTest(headerValue, state, name, input, nonce); expect(mockPost).toHaveBeenCalledWith(expectedEndpoint, { input }); }); @@ -751,16 +758,40 @@ describe('executeServiceTest', () => { const state = ServiceState.Active; const name = 'test-service'; const input = 'test-input'; + const nonce = 'test-nonce'; const expectedResponse = { success: true, data: 'test-data' }; mockPost.mockResolvedValue(expectedResponse); - const result = await executeServiceTest(headerValue, state, name, input); + const result = await executeServiceTest(headerValue, state, name, input, nonce); expect(result).toBe(expectedResponse); }); }); +describe('fetchNonce', () => { + let mockApiGet: ReturnType; + let mockGetNewNonce: ReturnType; + + beforeEach(async () => { + vi.clearAllMocks(); + mockApiGet = vi.mocked((await import('./api')).default.get); + mockGetNewNonce = vi.mocked((await import('resources/api-constants')).getNewNonce); + + mockGetNewNonce.mockReturnValue('/get-new-nonce'); + }); + + it('should call getNewNonce and return the nonce from the response', async () => { + mockApiGet.mockResolvedValue({ data: 'abc123' }); + + const result = await fetchNonce(); + + expect(mockGetNewNonce).toHaveBeenCalled(); + expect(mockApiGet).toHaveBeenCalledWith('/get-new-nonce'); + expect(result).toBe('abc123'); + }); +}); + describe('updateNodeTestState', () => { it('should update testingPassed for the specified node', () => { const mockServiceStore = { @@ -1231,11 +1262,12 @@ describe('executeService', () => { const state = ServiceState.Active; const name = 'test-service'; const input = 'test-input'; + const nonce = 'test-nonce'; - await executeService(state, name, input); + await executeService(state, name, input, nonce); expect(mockTestService).toHaveBeenCalledWith(state, name); - expect(mockApi).toHaveBeenCalledWith('/test-endpoint', { input }); + expect(mockApi).toHaveBeenCalledWith('/test-endpoint', { input }, { headers: { 'x-ruuter-nonce': nonce } }); }); }); diff --git a/GUI/src/services/service-tester.ts b/GUI/src/services/service-tester.ts index bb2d4a3f8..dd7a8ed41 100644 --- a/GUI/src/services/service-tester.ts +++ b/GUI/src/services/service-tester.ts @@ -1,6 +1,6 @@ import { Node } from '@xyflow/react'; import { t } from 'i18next'; -import { testService } from 'resources/api-constants'; +import { getNewNonce, testService } from 'resources/api-constants'; import useServiceStore, { ServiceStoreState } from 'store/new-services.store'; import useTestServiceStore from 'store/test-services.store'; import { NodeDataProps } from 'types/service-flow'; @@ -43,9 +43,11 @@ export const runServiceTest = async (input: string, serviceName?: string) => { const stateToUse = state == ServiceState.Ready ? ServiceState.Draft : state; try { - await executeServiceTest(headerValue, stateToUse, nameToUse, input.split(',')); + const testNonce = await fetchNonce(); + await executeServiceTest(headerValue, stateToUse, nameToUse, input.split(','), testNonce); - const response = await executeService(stateToUse, nameToUse, input.split(',')); + const runNonce = await fetchNonce(); + const response = await executeService(stateToUse, nameToUse, input.split(','), runNonce); addSuccessMessages(response.data); } catch (error) { @@ -53,6 +55,11 @@ export const runServiceTest = async (input: string, serviceName?: string) => { } }; +export const fetchNonce = async (): Promise => { + const response = await api.get(getNewNonce()); + return response.data; +}; + export function getInvalidNodes(nodes: Node[]): { label: string; error: string }[] { const invalidNodes: { label: string; error: string }[] = []; @@ -122,9 +129,16 @@ export const clearPreviousTestStates = (serviceStore: ServiceStoreState) => { ); }; -export const executeServiceTest = async (headerValue: string, state: ServiceState, name: string, input: string[]) => { +export const executeServiceTest = async ( + headerValue: string, + state: ServiceState, + name: string, + input: string[], + nonce: string, +) => { const testApi = createApiInstance({ 'x-ruuter-testing': headerValue, + 'x-ruuter-nonce': nonce, }); return testApi.post(testService(state, name), { input }); }; @@ -213,8 +227,8 @@ export function translateError(error: ServiceTestError, nodeLabel: string): Reco return translateObjectKeys(translatedError, 'chat.service-test-error'); } -export const executeService = async (state: ServiceState, name: string, input: string[]) => { - return api.post(testService(state, name), { input }); +export const executeService = async (state: ServiceState, name: string, input: string[], nonce: string) => { + return api.post(testService(state, name), { input }, { headers: { 'x-ruuter-nonce': nonce } }); }; export const addSuccessMessages = (responseData: ServiceResponse): void => { diff --git a/docker-compose.yml b/docker-compose.yml index e1fd4b9a9..8796e458f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - application.internalRequests.disabled=true - application.internalRequests.allowedIPs=127.0.0.1 - application.apiRequestTestingKey=voorshpellhappilo + - application.externalAuthAllowed=services/active/*,services/draft/*,services/inactive/* volumes: - ./DSL/Ruuter:/DSL - ./constants.ini:/app/constants.ini