Skip to content

Commit

Permalink
fix: check all doc access for app-attachment against the app entity
Browse files Browse the repository at this point in the history
  • Loading branch information
sleidig committed Sep 11, 2024
1 parent fe8d6a7 commit 66e776d
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 48 deletions.
104 changes: 104 additions & 0 deletions src/permissions/permission/permission.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@ import { PermissionService } from './permission.service';
import { DocumentRule, RulesService } from '../rules/rules.service';
import { UserInfo } from '../../restricted-endpoints/session/user-auth.dto';
import { DatabaseDocument } from '../../restricted-endpoints/replication/bulk-document/couchdb-dtos/bulk-docs.dto';
import { CouchdbService } from '../../couchdb/couchdb.service';
import { of } from 'rxjs';

describe('PermissionService', () => {
let service: PermissionService;
let mockRulesService: RulesService;
let mockCouchDBService: CouchdbService;
let normalUser: UserInfo;

beforeEach(async () => {
mockRulesService = {
getRulesForUser: () => undefined,
} as any;
mockCouchDBService = {
get: () => of({}),
} as any;

const module: TestingModule = await Test.createTestingModule({
providers: [
PermissionService,
{ provide: RulesService, useValue: mockRulesService },
{ provide: CouchdbService, useValue: mockCouchDBService },
],
}).compile();

Expand Down Expand Up @@ -85,4 +92,101 @@ describe('PermissionService', () => {
expect(ability.can('update', childDoc)).toBe(true);
expect(ability.can('read', childDoc)).toBe(true);
});

it('should confirm isAllowedTo to read a document if the user has the right permissions', async () => {
jest
.spyOn(mockRulesService, 'getRulesForUser')
.mockReturnValue([{ action: 'read', subject: 'Aser' }]);

const aserDoc: DatabaseDocument = { _id: 'Aser:someId', _rev: 'someRev' };

const result = await service.isAllowedTo('read', aserDoc, normalUser, 'db');
expect(result).toBe(true);
});

it('should deny isAllowedTo to read a document if the user has wrong permissions', async () => {
jest
.spyOn(mockRulesService, 'getRulesForUser')
.mockReturnValue([{ action: 'read', subject: 'Child' }]);

const aserDoc: DatabaseDocument = { _id: 'Aser:someId', _rev: 'someRev' };

const result = await service.isAllowedTo('read', aserDoc, normalUser, 'db');
expect(result).toBe(false);
});

it('should return isAllowedTo to for app-attachment based on check for the actual app entity', async () => {
jest
.spyOn(mockRulesService, 'getRulesForUser')
.mockReturnValue([
{ action: 'read', subject: 'Aser', conditions: { x: true } },
]);

const allowedEntityDoc: DatabaseDocument = {
_id: 'Aser:someId',
_rev: 'someRev',
x: true,
};
jest.spyOn(mockCouchDBService, 'get').mockReturnValue(of(allowedEntityDoc));

const attachmentDoc: DatabaseDocument = {
_id: 'Aser:someId',
_rev: 'attRev',
};

const result = await service.isAllowedTo(
'read',
attachmentDoc,
normalUser,
'app-attachments',
);
expect(result).toBe(true);
expect(mockCouchDBService.get).toHaveBeenCalledWith(
'app',
attachmentDoc._id,
);

const deniedEntityDoc: DatabaseDocument = {
_id: 'Aser:someId',
_rev: 'someRev',
x: false,
};
jest.spyOn(mockCouchDBService, 'get').mockReturnValue(of(deniedEntityDoc));
const result2 = await service.isAllowedTo(
'read',
attachmentDoc,
normalUser,
'app-attachments',
);
expect(result2).toBe(false);
});

it('should return isAllowedTo to for app-attachment based "update" action for entity', async () => {
jest
.spyOn(mockRulesService, 'getRulesForUser')
.mockReturnValue([{ action: 'update', subject: 'Aser' }]);

const entityDoc: DatabaseDocument = {
_id: 'Aser:someId',
_rev: 'someRev',
};
jest.spyOn(mockCouchDBService, 'get').mockReturnValue(of(entityDoc));

const attachmentDoc: DatabaseDocument = {
_id: 'Aser:someId',
_rev: 'attRev',
};

const result = await service.isAllowedTo(
'create',
attachmentDoc,
normalUser,
'app-attachments',
);
expect(result).toBe(true);
expect(mockCouchDBService.get).toHaveBeenCalledWith(
'app',
attachmentDoc._id,
);
});
});
36 changes: 35 additions & 1 deletion src/permissions/permission/permission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { UserInfo } from '../../restricted-endpoints/session/user-auth.dto';
import { RulesService } from '../rules/rules.service';
import { Ability, AbilityClass, InferSubjects } from '@casl/ability';
import { DatabaseDocument } from '../../restricted-endpoints/replication/bulk-document/couchdb-dtos/bulk-docs.dto';
import { firstValueFrom } from 'rxjs';
import { CouchdbService } from '../../couchdb/couchdb.service';

const actions = [
'read',
Expand All @@ -28,7 +30,10 @@ export function detectDocumentType(subject: DatabaseDocument): string {
*/
@Injectable()
export class PermissionService {
constructor(private rulesService: RulesService) {}
constructor(
private rulesService: RulesService,
private couchdbService: CouchdbService,
) {}

/**
* Creates an ability object containing all rules that are defined for the roles of the given user.
Expand All @@ -43,4 +48,33 @@ export class PermissionService {
detectSubjectType: detectDocumentType,
});
}

async isAllowedTo(
action: Action,
documentToAccess: DatabaseDocument,
user: UserInfo,
db: string,
): Promise<boolean> {
const userAbility = this.getAbilityFor(user);

let documentForPermissionCheck: DatabaseDocument = documentToAccess;
let actionForPermissionCheck: Action = action;

if (db === 'app-attachments') {
// check permissions on the actual, full entity so that special condition rules can be applied
documentForPermissionCheck = await firstValueFrom(
this.couchdbService.get('app', documentToAccess._id),
).catch(() => undefined);

// on app-attachments we are logically only editing a field of the entity, so update permission should be enough
if (action !== 'read') {
actionForPermissionCheck = 'update';
}
}

return userAbility.can(
actionForPermissionCheck,
documentForPermissionCheck,
);
}
}
Loading

0 comments on commit 66e776d

Please sign in to comment.