77 ActionEntity ,
88 ActionTemplateEntity ,
99 ApiKeyEntity ,
10+ FileEntity ,
1011 MissionEntity ,
1112 ProjectEntity ,
1213 UserEntity ,
@@ -18,6 +19,7 @@ import {
1819 createMissionUsingPost ,
1920 createProjectUsingPost ,
2021 HeaderCreator ,
22+ uploadFile ,
2123} from '../utils/api-calls' ;
2224import { clearAllData , database } from '../utils/database-utilities' ;
2325
@@ -295,4 +297,158 @@ describe('Verify Action API Key Scope', () => {
295297
296298 expect ( projectResponse . status ) . toBe ( 403 ) ; // Forbidden
297299 } ) ;
300+
301+ test ( 'if an action API key CANNOT move a file to another mission (even in the same project)' , async ( ) => {
302+ // 1. Submit Action in the original mission
303+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
304+ const headers = new HeaderCreator ( globalThis . creator ) ;
305+ headers . addHeader ( 'Content-Type' , 'application/json' ) ;
306+ const submitResponse = await fetch ( `${ DEFAULT_URL } /actions` , {
307+ method : 'POST' ,
308+ headers : headers . getHeaders ( ) ,
309+ body : JSON . stringify ( {
310+ missionUUID : globalThis . missionUuid ,
311+ templateUUID : globalThis . templateUuid ,
312+ } as SubmitActionDto ) ,
313+ } ) ;
314+ expect ( submitResponse . status ) . toBe ( 201 ) ;
315+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
316+ const { actionUUID : uuid } = await submitResponse . json ( ) ;
317+
318+ // 2. Get API Key
319+ const actionRepo = database . getRepository ( ActionEntity ) ;
320+ const action = await actionRepo . findOneOrFail ( {
321+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
322+ where : { uuid } ,
323+ } ) ;
324+
325+ const apiKeyRepo = database . getRepository ( ApiKeyEntity ) ;
326+ const apiKeyEntity = apiKeyRepo . create ( {
327+ // eslint-disable-next-line @typescript-eslint/naming-convention
328+ key_type : KeyTypes . ACTION ,
329+ mission : { uuid : globalThis . missionUuid } ,
330+ action : action ,
331+ rights : AccessGroupRights . WRITE ,
332+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
333+ user : globalThis . creator ,
334+ } ) ;
335+ await apiKeyRepo . save ( apiKeyEntity ) ;
336+ const apiKey = apiKeyEntity . apikey ;
337+
338+ // 3. Create another mission in the same project
339+ const otherMissionUuid = await createMissionUsingPost (
340+ {
341+ name : 'other_mission_same_project' ,
342+ projectUUID : globalThis . projectUuid ,
343+ tags : { } ,
344+ ignoreTags : true ,
345+ } ,
346+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
347+ globalThis . creator ,
348+ ) ;
349+ // 4. Upload a file in the original mission using the helper (so it exists on S3)
350+ await uploadFile (
351+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
352+ globalThis . creator ,
353+ 'test.bag' ,
354+ globalThis . missionUuid ,
355+ ) ;
356+ const fileRepo = database . getRepository ( FileEntity ) ;
357+ const file = await fileRepo . findOneByOrFail ( { filename : 'test.bag' } ) ;
358+
359+ // 5. Try to move file to the other mission using PUT /files/:uuid
360+ const moveResponse = await fetch ( `${ DEFAULT_URL } /files/${ file . uuid } ` , {
361+ method : 'PUT' ,
362+ headers : {
363+ // eslint-disable-next-line @typescript-eslint/naming-convention
364+ 'Content-Type' : 'application/json' ,
365+ // eslint-disable-next-line @typescript-eslint/naming-convention
366+ 'x-api-key' : apiKey ,
367+ // eslint-disable-next-line @typescript-eslint/naming-convention
368+ 'kleinkram-client-version' : appVersion ,
369+ } ,
370+ body : JSON . stringify ( {
371+ uuid : file . uuid ,
372+ filename : 'test.bag' ,
373+ date : file . date . toISOString ( ) ,
374+ missionUuid : otherMissionUuid ,
375+ } ) ,
376+ } ) ;
377+
378+ expect ( moveResponse . status ) . toBe ( 403 ) ; // Forbidden
379+ } ) ;
380+
381+ test ( 'if an action API key CAN update a file filename within its own mission' , async ( ) => {
382+ // 1. Submit Action in the original mission
383+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
384+ const headers = new HeaderCreator ( globalThis . creator ) ;
385+ headers . addHeader ( 'Content-Type' , 'application/json' ) ;
386+ const submitResponse = await fetch ( `${ DEFAULT_URL } /actions` , {
387+ method : 'POST' ,
388+ headers : headers . getHeaders ( ) ,
389+ body : JSON . stringify ( {
390+ missionUUID : globalThis . missionUuid ,
391+ templateUUID : globalThis . templateUuid ,
392+ } as SubmitActionDto ) ,
393+ } ) ;
394+ expect ( submitResponse . status ) . toBe ( 201 ) ;
395+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
396+ const { actionUUID : uuid } = await submitResponse . json ( ) ;
397+
398+ // 2. Get API Key
399+ const actionRepo = database . getRepository ( ActionEntity ) ;
400+ const action = await actionRepo . findOneOrFail ( {
401+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
402+ where : { uuid } ,
403+ } ) ;
404+
405+ const apiKeyRepo = database . getRepository ( ApiKeyEntity ) ;
406+ const apiKeyEntity = apiKeyRepo . create ( {
407+ // eslint-disable-next-line @typescript-eslint/naming-convention
408+ key_type : KeyTypes . ACTION ,
409+ mission : { uuid : globalThis . missionUuid } ,
410+ action : action ,
411+ rights : AccessGroupRights . WRITE ,
412+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
413+ user : globalThis . creator ,
414+ } ) ;
415+ await apiKeyRepo . save ( apiKeyEntity ) ;
416+ const apiKey = apiKeyEntity . apikey ;
417+
418+ // 3. Upload a file in the original mission using the helper (so it exists on S3)
419+ await uploadFile (
420+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
421+ globalThis . creator ,
422+ 'test.bag' ,
423+ globalThis . missionUuid ,
424+ ) ;
425+ const fileRepo = database . getRepository ( FileEntity ) ;
426+ const file = await fileRepo . findOneByOrFail ( { filename : 'test.bag' } ) ;
427+
428+ // 4. Update file filename within the same mission using PUT /files/:uuid
429+ const updateResponse = await fetch (
430+ `${ DEFAULT_URL } /files/${ file . uuid } ` ,
431+ {
432+ method : 'PUT' ,
433+ headers : {
434+ // eslint-disable-next-line @typescript-eslint/naming-convention
435+ 'Content-Type' : 'application/json' ,
436+ // eslint-disable-next-line @typescript-eslint/naming-convention
437+ 'x-api-key' : apiKey ,
438+ // eslint-disable-next-line @typescript-eslint/naming-convention
439+ 'kleinkram-client-version' : appVersion ,
440+ } ,
441+ body : JSON . stringify ( {
442+ uuid : file . uuid ,
443+ filename : 'updated.bag' ,
444+ date : file . date . toISOString ( ) ,
445+ missionUuid : globalThis . missionUuid ,
446+ } ) ,
447+ } ,
448+ ) ;
449+
450+ expect ( updateResponse . status ) . toBe ( 200 ) ;
451+ const updatedFile = await fileRepo . findOneByOrFail ( { uuid : file . uuid } ) ;
452+ expect ( updatedFile . filename ) . toBe ( 'updated.bag' ) ;
453+ } ) ;
298454} ) ;
0 commit comments