Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export class CustodyAccountController {
dto.description,
);

return CustodyAccountDtoMapper.toDto(custodyAccount, CustodyAccessLevel.WRITE);
// The caller just created it, so it is theirs by definition.
return CustodyAccountDtoMapper.toDto(custodyAccount, CustodyAccessLevel.WRITE, true);
}

@Put(':id')
Expand All @@ -107,7 +108,13 @@ export class CustodyAccountController {
dto.description,
);

return CustodyAccountDtoMapper.toDto(custodyAccount, CustodyAccessLevel.WRITE);
// Reached through the write guard, which a grantee passes too — ownership is a separate
// question from the level and has to be answered from the account itself.
return CustodyAccountDtoMapper.toDto(
custodyAccount,
CustodyAccessLevel.WRITE,
custodyAccount.isOwnedBy(jwt.account),
);
}

@Get(':id/balance')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export class CustodyAccountDto {
@ApiProperty({ enum: CustodyAccessLevel, description: 'Access level for current user' })
accessLevel: CustodyAccessLevel;

@ApiProperty({ description: 'Whether the current user owns this account rather than being granted access to it' })
isOwner: boolean;

@ApiPropertyOptional({ type: CustodyUserDto })
owner?: CustodyUserDto;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity';
import { CustodyAccessLevel, CustodyAccountStatus } from '../../enums/custody';
import { CustodyAccountAccess } from '../custody-account-access.entity';
import { CustodyAccount } from '../custody-account.entity';

describe('CustodyAccount', () => {
const ownerId = 100;
const granteeId = 200;
const strangerId = 300;
const custodyAccountId = 1;

function userData(overrides: Partial<UserData> = {}): UserData {
return Object.assign(new UserData(), { id: ownerId, users: [], custodyAccounts: [], ...overrides });
}

function custodyAccount(overrides: Partial<CustodyAccount> = {}): CustodyAccount {
return Object.assign(new CustodyAccount(), {
id: custodyAccountId,
title: 'Own Safe',
description: 'Owner account',
owner: userData(),
requiredSignatures: 1,
status: CustodyAccountStatus.ACTIVE,
accessGrants: [],
...overrides,
});
}

function accessGrant(params: {
id?: number;
account: CustodyAccount;
userData: UserData;
accessLevel: CustodyAccessLevel;
active: boolean;
}): CustodyAccountAccess {
return Object.assign(new CustodyAccountAccess(), {
id: params.id ?? 10,
account: params.account,
userData: params.userData,
accessLevel: params.accessLevel,
active: params.active,
});
}

describe('#isOwnedBy(...)', () => {
it("returns true for the owner's user-data id", () => {
const account = custodyAccount();

expect(account.isOwnedBy(ownerId)).toBe(true);
});

it('returns false for a grantee with an active write grant on the account', () => {
const account = custodyAccount();
const grant = accessGrant({
account,
userData: userData({ id: granteeId }),
accessLevel: CustodyAccessLevel.WRITE,
active: true,
});
account.accessGrants = [grant];

expect(account.isOwnedBy(granteeId)).toBe(false);
});

it('returns false for a user-data id with no ownership or access grant', () => {
const account = custodyAccount();
const grant = accessGrant({
account,
userData: userData({ id: granteeId }),
accessLevel: CustodyAccessLevel.WRITE,
active: true,
});
account.accessGrants = [grant];

expect(account.isOwnedBy(strangerId)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@ export class CustodyAccount extends IEntity {

@OneToMany(() => CustodyAccountAccess, (access) => access.account)
accessGrants: CustodyAccountAccess[];

/**
* Whether this account belongs to the given user_data, as opposed to merely being reachable
* through a grant. Distinct from the access level: a grantee can hold WRITE without owning
* anything, and an owner can narrow themselves to READ while still owning it.
*/
isOwnedBy(accountId: number): boolean {
return this.owner.id === accountId;
}
}
Loading
Loading