Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug: Restrict user data access in User query #2623

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
27 changes: 24 additions & 3 deletions src/resolvers/Query/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { AppUserProfile, User } from "../../models";
import type { QueryResolvers } from "../../types/generatedGraphQLTypes";
/**
* This query fetch the user from the database.
*
* This function ensure that users can only query their own data and not access details of other users , protecting sensitive data.
*
* @param _parent-
* @param args - An object that contains `id` for the user.
* @param context-
* @returns An object that contains user data. If the user is not found then it throws a `NotFoundError` error.
*/
export const user: QueryResolvers["user"] = async (_parent, args, context) => {
// Check if the current user exists in the system
const currentUserExists = !!(await User.exists({
_id: context.userId,
}));
Expand All @@ -23,9 +27,26 @@ export const user: QueryResolvers["user"] = async (_parent, args, context) => {
);
}

const user: InterfaceUser = (await User.findOne({
_id: args.id,
}).lean()) as InterfaceUser;
// Check if the requesting user is trying to access their own data
if (context.userId !== args.id) {
throw new errors.UnauthorizedError(
"Access denied. You can only view your own profile.",
);
}

// Fetch the user data from the database based on the provided ID (args.id)
const user: InterfaceUser = (await User.findById(
args.id,
).lean()) as InterfaceUser;

if (!user) {
throw new errors.NotFoundError(
USER_NOT_FOUND_ERROR.DESC,
USER_NOT_FOUND_ERROR.CODE,
USER_NOT_FOUND_ERROR.PARAM,
);
}

const userAppProfile: InterfaceAppUserProfile = (await AppUserProfile.findOne(
{
userId: user._id,
Expand Down
74 changes: 74 additions & 0 deletions tests/resolvers/Query/userAccess.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { USER_NOT_FOUND_ERROR } from "../../../src/constants";
import type mongoose from "mongoose";
import { user } from "../../../src/resolvers/Query/user";
import { connect, disconnect } from "../../helpers/db";
import type { TestUserType } from "../../helpers/userAndOrg";
import { createTestUser } from "../../helpers/userAndOrg";
import { beforeAll, afterAll, describe, it, expect } from "vitest";

let testUser: TestUserType;
let anotherTestUser: TestUserType;
let MONGOOSE_INSTANCE: typeof mongoose;

beforeAll(async () => {
MONGOOSE_INSTANCE = await connect();
testUser = await createTestUser();
anotherTestUser = await createTestUser();
});

afterAll(async () => {
await disconnect(MONGOOSE_INSTANCE);
});

describe("user Query", () => {
// Test case 1: Invalid user ID scenario
it("throws error if user doesn't exist", async () => {
const args = {
id: "invalidUserId",
};

const context = {
userId: testUser?.id,
};

try {
await user?.({}, args, context);
} catch (error: unknown) {
expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.DESC);
}
});

// Test case 2: Unauthorized access scenario
it("throws unauthorized error when trying to access another user's data", async () => {
const args = {
id: anotherTestUser?.id,
};

const context = {
userId: testUser?.id,
};

try {
await user?.({}, args, context);
} catch (error: unknown) {
expect((error as Error).message).toEqual(
"Access denied. You can only view your own profile.",
);
Comment on lines +94 to +96
Copy link

@coderabbitai coderabbitai bot Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move error message to constants.

The error message for unauthorized access should be defined in the constants file for consistency and maintainability.

-        "Access denied. You can only view your own profile.",
+        UNAUTHORIZED_ACCESS_ERROR.DESC,

Add to constants file:

export const UNAUTHORIZED_ACCESS_ERROR = {
  CODE: 'UNAUTHORIZED_ACCESS',
  DESC: 'Access denied. You can only view your own profile.'
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nitintwt, please fix this. We don't want hardcoded text directly inside the code. Check if any similar message is already there.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}
});

// Test case 3: Successful access scenario
it("successfully returns user data when accessing own profile", async () => {
const args = {
id: testUser?.id,
};

const context = {
userId: testUser?.id,
};

const result = await user?.({}, args, context);

expect(result).toBeDefined();
});
});