fix: use team.findMany for fetching user teams (#6057)
This commit is contained in:
parent
d5a19320b8
commit
8ac1b29b88
5 changed files with 79 additions and 66 deletions
|
|
@ -3,65 +3,95 @@ import { isValidLocalhostRedirectUri } from './redirect-uri.validator';
|
|||
describe('isValidLocalhostRedirectUri', () => {
|
||||
describe('valid loopback URIs', () => {
|
||||
test('Should return true for http://localhost with a port', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhost:3000/device-token')).toBe(true);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://localhost:3000/device-token'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return true for http://localhost without a port', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhost/callback')).toBe(true);
|
||||
expect(isValidLocalhostRedirectUri('http://localhost/callback')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return true for http://127.0.0.1 with a port', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://127.0.0.1:8080/callback')).toBe(true);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://127.0.0.1:8080/callback'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return true for http://[::1] IPv6 loopback', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://[::1]:9090/callback')).toBe(true);
|
||||
expect(isValidLocalhostRedirectUri('http://[::1]:9090/callback')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DNS wildcard bypass vectors', () => {
|
||||
test('Should reject localhost subdomain via sslip.io wildcard DNS', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhost.1.2.3.4.sslip.io:3000/steal')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri(
|
||||
'http://localhost.1.2.3.4.sslip.io:3000/steal',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject localhost subdomain via nip.io wildcard DNS', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhost.10.0.0.1.nip.io/steal')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://localhost.10.0.0.1.nip.io/steal'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject localhost as a subdomain of attacker domain', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhost.evil.com/steal')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://localhost.evil.com/steal'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject localhost with trailing dot FQDN trick', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhost./callback')).toBe(false);
|
||||
expect(isValidLocalhostRedirectUri('http://localhost./callback')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('Should reject domain that starts with localhost string', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://localhostevil.com/callback')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://localhostevil.com/callback'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('protocol enforcement', () => {
|
||||
test('Should reject https since loopback listeners do not serve TLS', () => {
|
||||
expect(isValidLocalhostRedirectUri('https://localhost:3000/callback')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('https://localhost:3000/callback'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject ftp protocol', () => {
|
||||
expect(isValidLocalhostRedirectUri('ftp://localhost/callback')).toBe(false);
|
||||
expect(isValidLocalhostRedirectUri('ftp://localhost/callback')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('credential and remote host rejection', () => {
|
||||
test('Should reject URL with embedded credentials', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://user:pass@localhost:3000/callback')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://user:pass@localhost:3000/callback'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject an arbitrary remote host', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://attacker.com:3000/callback')).toBe(false);
|
||||
expect(
|
||||
isValidLocalhostRedirectUri('http://attacker.com:3000/callback'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Should reject 0.0.0.0 since it is not a loopback address', () => {
|
||||
expect(isValidLocalhostRedirectUri('http://0.0.0.0:3000/callback')).toBe(false);
|
||||
expect(isValidLocalhostRedirectUri('http://0.0.0.0:3000/callback')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Only true loopback addresses are safe for native app redirects
|
||||
const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
|
||||
|
||||
export function isValidLocalhostRedirectUri(uri: string | undefined | null): boolean {
|
||||
export function isValidLocalhostRedirectUri(
|
||||
uri: string | undefined | null,
|
||||
): boolean {
|
||||
if (!uri) return false;
|
||||
|
||||
let url: URL;
|
||||
|
|
|
|||
|
|
@ -177,10 +177,7 @@ export class MockServerController {
|
|||
// Security headers to prevent XSS via mock responses
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
if (!isSubdomainAccess) {
|
||||
res.setHeader(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'none'; sandbox",
|
||||
);
|
||||
res.setHeader('Content-Security-Policy', "default-src 'none'; sandbox");
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -800,17 +800,20 @@ describe('getMembersOfTeam', () => {
|
|||
|
||||
describe('getTeamsOfUser', () => {
|
||||
test('resolves with the first 10 elements when no cursor is given', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
mockPrisma.team.findMany.mockResolvedValue([]);
|
||||
|
||||
await teamService.getTeamsOfUser(dbTeamMember.userUid, null);
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
expect(mockPrisma.team.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
skip: 0,
|
||||
cursor: undefined,
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
members: {
|
||||
some: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -818,30 +821,28 @@ describe('getTeamsOfUser', () => {
|
|||
test('resolves as expected for paginated requests with cursor', async () => {
|
||||
const cursor = 'secondpage';
|
||||
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
mockPrisma.team.findMany.mockResolvedValue([]);
|
||||
await teamService.getTeamsOfUser(dbTeamMember.userUid, cursor);
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
expect(mockPrisma.team.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
teamID_userUid: {
|
||||
teamID: cursor,
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
id: cursor,
|
||||
},
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
members: {
|
||||
some: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves with an empty array for an invalid cursor', () => {
|
||||
// Invalid cursors return an empty array
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
mockPrisma.team.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamsOfUser(dbTeamMember.userUid, 'invalidcursor'),
|
||||
|
|
@ -849,7 +850,7 @@ describe('getTeamsOfUser', () => {
|
|||
});
|
||||
|
||||
test('resolves with an empty array for invalid user id and null cursor', () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
mockPrisma.team.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamsOfUser('invalidid', null),
|
||||
|
|
@ -857,7 +858,7 @@ describe('getTeamsOfUser', () => {
|
|||
});
|
||||
|
||||
test('resolves with an empty array for invalid user id and invalid cursor', () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
mockPrisma.team.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamsOfUser('invalidId', 'invalidCursor'),
|
||||
|
|
|
|||
|
|
@ -266,37 +266,20 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||
cursor: string | null,
|
||||
take = 10,
|
||||
): Promise<Team[]> {
|
||||
if (!cursor) {
|
||||
const entries = await this.prisma.teamMember.findMany({
|
||||
take,
|
||||
where: {
|
||||
userUid: uid,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
return entries.map((entry) => entry.team);
|
||||
} else {
|
||||
const entries = await this.prisma.teamMember.findMany({
|
||||
take,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
teamID_userUid: {
|
||||
teamID: cursor,
|
||||
const teams = await this.prisma.team.findMany({
|
||||
take,
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userUid: uid,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
userUid: uid,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
return entries.map((entry) => entry.team);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
async getTeamWithID(teamID: string): Promise<Team | null> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue