-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add role based access control (#564)
- Loading branch information
1 parent
95bb1f9
commit eca8909
Showing
25 changed files
with
719 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import UserRoles from "supertokens-node/recipe/userroles"; | ||
|
||
import { ROLE_SUPER_ADMIN } from "../constants"; | ||
|
||
import type { FastifyInstance } from "fastify"; | ||
|
||
const getPermissions = async (roles: string[]) => { | ||
let permissions: string[] = []; | ||
|
||
for (const role of roles) { | ||
const response = await UserRoles.getPermissionsForRole(role); | ||
|
||
if (response.status === "OK") { | ||
permissions = [...new Set([...permissions, ...response.permissions])]; | ||
} | ||
} | ||
|
||
return permissions; | ||
}; | ||
|
||
const hasUserPermission = async ( | ||
fastify: FastifyInstance, | ||
userId: string, | ||
permission: string | ||
): Promise<boolean> => { | ||
const permissions = fastify.config.user.permissions; | ||
|
||
// Allow if provided permission is not defined | ||
if (!permissions || !permissions.includes(permission)) { | ||
return true; | ||
} | ||
|
||
const { roles } = await UserRoles.getRolesForUser(userId); | ||
|
||
// Allow if user has super admin role | ||
if (roles && roles.includes(ROLE_SUPER_ADMIN)) { | ||
return true; | ||
} | ||
|
||
const rolePermissions = await getPermissions(roles); | ||
|
||
if (!rolePermissions || !rolePermissions.includes(permission)) { | ||
return false; | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
export default hasUserPermission; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import FastifyPlugin from "fastify-plugin"; | ||
import mercurius from "mercurius"; | ||
import mercuriusAuth from "mercurius-auth"; | ||
import emailVerificaiton from "supertokens-node/recipe/emailverification"; | ||
|
||
import type { FastifyInstance } from "fastify"; | ||
|
||
const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { | ||
await fastify.register(mercuriusAuth, { | ||
async applyPolicy(authDirectiveAST, parent, arguments_, context) { | ||
if (!context.user) { | ||
return new mercurius.ErrorWithProps("unauthorized", {}, 401); | ||
} | ||
|
||
if (context.user.disabled) { | ||
return new mercurius.ErrorWithProps("user is disabled", {}, 401); | ||
} | ||
|
||
if ( | ||
fastify.config.user.features?.signUp?.emailVerification && | ||
!(await emailVerificaiton.isEmailVerified(context.user.id)) | ||
) { | ||
// Added the claim validation errors to match with rest endpoint | ||
// response for email verification | ||
return new mercurius.ErrorWithProps( | ||
"invalid claim", | ||
{ | ||
claimValidationErrors: [ | ||
{ | ||
id: "st-ev", | ||
reason: { | ||
message: "wrong value", | ||
expectedValue: true, | ||
actualValue: false, | ||
}, | ||
}, | ||
], | ||
}, | ||
403 | ||
); | ||
} | ||
|
||
return true; | ||
}, | ||
|
||
authDirective: "auth", | ||
}); | ||
}); | ||
|
||
export default plugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import FastifyPlugin from "fastify-plugin"; | ||
import mercurius from "mercurius"; | ||
import mercuriusAuth from "mercurius-auth"; | ||
|
||
import hasUserPermission from "../lib/hasUserPermission"; | ||
|
||
import type { FastifyInstance } from "fastify"; | ||
|
||
const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { | ||
await fastify.register(mercuriusAuth, { | ||
applyPolicy: async (authDirectiveAST, parent, arguments_, context) => { | ||
const permission = authDirectiveAST.arguments.find( | ||
(argument: { name: { value: string } }) => | ||
argument.name.value === "permission" | ||
).value.value; | ||
|
||
if (!context.user) { | ||
return new mercurius.ErrorWithProps("unauthorized", {}, 401); | ||
} | ||
|
||
const hasPermission = await hasUserPermission( | ||
context.app, | ||
context.user?.id, | ||
permission | ||
); | ||
|
||
if (!hasPermission) { | ||
// Added the claim validation errors to match with rest endpoint | ||
// response for hasPermission | ||
return new mercurius.ErrorWithProps( | ||
"invalid claim", | ||
{ | ||
claimValidationErrors: [ | ||
{ | ||
id: "st-perm", | ||
reason: { | ||
message: "Not have enough permission", | ||
expectedToInclude: permission, | ||
}, | ||
}, | ||
], | ||
}, | ||
403 | ||
); | ||
} | ||
|
||
return true; | ||
}, | ||
authDirective: "hasPermission", | ||
}); | ||
}); | ||
|
||
export default plugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Error as STError } from "supertokens-node/recipe/session"; | ||
import UserRoles from "supertokens-node/recipe/userroles"; | ||
|
||
import hasUserPermission from "../lib/hasUserPermission"; | ||
|
||
import type { SessionRequest } from "supertokens-node/framework/fastify"; | ||
|
||
const hasPermission = | ||
(permission: string) => | ||
async (request: SessionRequest): Promise<void> => { | ||
const userId = request.session?.getUserId(); | ||
|
||
if (!userId) { | ||
throw new STError({ | ||
type: "UNAUTHORISED", | ||
message: "unauthorised", | ||
}); | ||
} | ||
|
||
if (!(await hasUserPermission(request.server, userId, permission))) { | ||
// this error tells SuperTokens to return a 403 http response. | ||
throw new STError({ | ||
type: "INVALID_CLAIMS", | ||
message: "Not have enough permission", | ||
payload: [ | ||
{ | ||
id: UserRoles.PermissionClaim.key, | ||
reason: { | ||
message: "Not have enough permission", | ||
expectedToInclude: permission, | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
}; | ||
|
||
export default hasPermission; |
Oops, something went wrong.