Skip to content

Commit d72daa3

Browse files
committed
Changed so an admin can't change a user's password
1 parent 746c72a commit d72daa3

File tree

5 files changed

+97
-11
lines changed

5 files changed

+97
-11
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,16 @@ Users can modify or view their own data. Admins can do anything except refresh a
106106

107107
The `UsernameEmailGuard` compares the user's email or username with the same field in a query. If any query or mutation in the resolver has `doAnythingWithUser(username: string)` or `doAnythingWithUser(email: string)` and that email / username matches the user which is requesting the action, it will be approved. Username and email are unique, and the user has already been verified via JWT. **If there is not a username or email in the request, it will pass.** This is because the resolvers will set the action on the user making the request. For example, on updateUser if no username is specified, the modification is on the user making the request.
108108

109-
The `UsernameEmailAdminGuard` is the same as the `UsernameEmailGuard` except it also allows admins.
109+
The `UsernameEmailAdminGuard` is the same as the `UsernameEmailGuard` except it also allows admins. Admins should not be allowed to change everything. For example, an admin should not be allowed to set another user's password. This would allow the admin to impersonate that user. The `@AdminAllowedArgs` decorator has been added for this reason to this guard. If this decorator is used, only the arguments specified are allowed. Placing the below decorator above the `updateUser` resolver will not allow an admin to specify the `fieldsToUpdate.password` argument.
110+
111+
```Typescript
112+
@AdminAllowedArgs(
113+
'username',
114+
'fieldsToUpdate.username',
115+
'fieldsToUpdate.email',
116+
'fieldsToUpdate.enabled',
117+
)
118+
```
110119

111120
The `AdminGuard` only allows admins.
112121

src/auth/guards/username-email-admin.guard.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,40 @@ import { GqlExecutionContext } from '@nestjs/graphql';
33
import { User } from '../../graphql.classes';
44
import { UsersService } from '../../users/users.service';
55
import { AuthenticationError } from 'apollo-server-core';
6+
import { Reflector } from '@nestjs/core';
67

78
// Check if username in field for query matches authenticated user's username
89
// or if the user is admin
910
@Injectable()
1011
export class UsernameEmailAdminGuard implements CanActivate {
11-
constructor(private usersService: UsersService) {}
12+
constructor(
13+
private usersService: UsersService,
14+
private readonly reflector: Reflector,
15+
) {}
16+
17+
// Returns an array of all the properties of an object seperated by a .
18+
getPropertiesArray(object: any): string[] {
19+
let result: string[] = [];
20+
Object.entries(object).forEach(([key, value]) => {
21+
const field = key;
22+
if (typeof value === 'object' && value !== null) {
23+
const objectProperties = this.getPropertiesArray(value).map(
24+
prop => `${field}.${prop}`,
25+
);
26+
result = result.concat(objectProperties);
27+
} else {
28+
result.push(field);
29+
}
30+
});
31+
return result;
32+
}
1233

1334
canActivate(context: ExecutionContext): boolean {
1435
const ctx = GqlExecutionContext.create(context);
1536
const request = ctx.getContext().req;
1637
let shouldActivate = false;
1738
if (request.user) {
1839
const user = <User> request.user;
19-
if (this.usersService.isAdmin(user.permissions)) return true;
2040
const args = ctx.getArgs();
2141
if (args.username && typeof args.username === 'string') {
2242
shouldActivate =
@@ -26,6 +46,29 @@ export class UsernameEmailAdminGuard implements CanActivate {
2646
} else if (!args.username && !args.email) {
2747
shouldActivate = true;
2848
}
49+
50+
if (
51+
shouldActivate === false &&
52+
this.usersService.isAdmin(user.permissions)
53+
) {
54+
const adminAllowedArgs = this.reflector.get<string[]>(
55+
'adminAllowedArgs',
56+
context.getHandler(),
57+
);
58+
59+
shouldActivate = true;
60+
61+
if (adminAllowedArgs) {
62+
const argFields = this.getPropertiesArray(args);
63+
argFields.forEach(field => {
64+
if (!adminAllowedArgs.includes(field)) {
65+
throw new AuthenticationError(
66+
`Admin is not allowed to modify ${field}`,
67+
);
68+
}
69+
});
70+
}
71+
}
2972
}
3073
if (!shouldActivate) {
3174
throw new AuthenticationError('Could not authenticate with token');

src/decorators/admin-allowed-args.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { SetMetadata } from '@nestjs/common';
2+
3+
export const AdminAllowedArgs = (...adminAllowedArgs: string[]) =>
4+
SetMetadata('adminAllowedArgs', adminAllowedArgs);

src/users/users.resolvers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { UsernameEmailAdminGuard } from '../auth/guards/username-email-admin.gua
77
import { AdminGuard } from '../auth/guards/admin.guard';
88
import { UserInputError, ValidationError } from 'apollo-server-core';
99
import { UserDocument } from './schemas/user.schema';
10+
import { AdminAllowedArgs } from '../decorators/admin-allowed-args';
1011

1112
@Resolver('User')
1213
export class UserResolver {
@@ -74,6 +75,12 @@ export class UserResolver {
7475
}
7576

7677
@Mutation('updateUser')
78+
@AdminAllowedArgs(
79+
'username',
80+
'fieldsToUpdate.username',
81+
'fieldsToUpdate.email',
82+
'fieldsToUpdate.enabled',
83+
)
7784
@UseGuards(JwtAuthGuard, UsernameEmailAdminGuard)
7885
async updateUser(
7986
@Args('username') username: string,

test/users.e2e-spec.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -876,10 +876,6 @@ describe('Users (e2e)', () => {
876876
fieldsToUpdate: {
877877
username: "newUsernameByAdmin",
878878
879-
password: {
880-
oldPassword: "password",
881-
newPassword: "newPassword",
882-
}
883879
}) {username, email}}`,
884880
};
885881

@@ -894,12 +890,39 @@ describe('Users (e2e)', () => {
894890
895891
});
896892
});
893+
});
897894

898-
const login = await authService.validateUserByPassword({
899-
username: 'newUsernameByAdmin',
900-
password: 'newPassword',
895+
it('fails with admin changing another user\'s password', async () => {
896+
await usersService.create({
897+
username: 'userToUpdateByAdmin2',
898+
899+
password: 'password',
901900
});
902-
expect(login).toBeTruthy();
901+
902+
const data = {
903+
query: `mutation {
904+
updateUser(
905+
username: "userToUpdateByAdmin2",
906+
fieldsToUpdate: {
907+
username: "newUsernameByAdmin2",
908+
909+
password: {
910+
oldPassword: "password",
911+
newPassword: "newPassword",
912+
}
913+
}) {username, email}}`,
914+
};
915+
916+
await request(app.getHttpServer())
917+
.post('/graphql')
918+
.set('Authorization', `Bearer ${adminLogin.token!}`)
919+
.send(data)
920+
.expect(200)
921+
.expect(response => {
922+
expect(response.body.errors[0].extensions.code).toEqual(
923+
'UNAUTHENTICATED',
924+
);
925+
});
903926
});
904927

905928
it('updates other fields with changed username already in use', async () => {

0 commit comments

Comments
 (0)