Skip to content

Commit

Permalink
RBAC flow UI implementation and user API endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
sanudutta45 committed Nov 9, 2024
1 parent 88c500e commit a3d410d
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 29 deletions.
7 changes: 5 additions & 2 deletions backend/api/AuthView.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from starlette.responses import JSONResponse
from backend.managers.AuthManager import AuthManager
from backend.managers.CasbinRoleManager import CasbinRoleManager
from backend.schemas import AuthOptionsRequest, RegistrationOptions, VerifyAuthentication, AuthenticationOptions, VerifyRegistration
from connexion import request
from uuid import uuid4
Expand All @@ -9,6 +10,7 @@
class AuthView:
def __init__(self):
self.am = AuthManager()
self.cb = CasbinRoleManager()

async def auth_options(self, body: AuthOptionsRequest):
challenge, options, type = await self.am.auth_options(body["email"])
Expand Down Expand Up @@ -53,11 +55,12 @@ async def webauthn_login_options(self, body: AuthenticationOptions):

async def webauthn_login(self, body: VerifyAuthentication):
challenge = request.cookies.get("challenge")
token = await self.am.webauthn_login(challenge, body["email"], body["auth_resp"])
token, role = await self.am.webauthn_login(challenge, body["email"], body["auth_resp"])
if not token:
return JSONResponse({"message": "Failed"}, status_code=401)

response = JSONResponse({"message": "Success", "token": token}, status_code=200)
permissions = self.cb.create_resource_access(role)
response = JSONResponse({"message": "Success", "token": token, "permissions": permissions}, status_code=200)
response.set_cookie(key="challenge",value="", expires=0,secure=True, httponly=True, samesite='strict')

return response
Expand Down
22 changes: 15 additions & 7 deletions backend/api/UsersView.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,40 @@ def __init__(self):
self.um = UsersManager()
self.cb = CasbinRoleManager()

async def get(self, id: str):
async def get(self, token_info, id: str):
if not self.cb.check_permissions(token_info["role"], 'user', 'show'):
return JSONResponse({"message": "Permission denied"}, status_code=403)

user = await self.um.retrieve_user(id)
if user is None:
return JSONResponse(status_code=404, headers={"error": "User not found"})
return JSONResponse(user.model_dump(), status_code=200)

async def post(self,token_info, body: dict):
enforcer = self.cb.get_enforcer()
if not enforcer.enforce(token_info["role"], 'user', 'POST'):
if not self.cb.check_permissions(token_info["role"], 'user', 'create'):
return JSONResponse({"message": "Permission denied"}, status_code=403)

try:
id = await self.um.create_user(body['name'], body['email'])
return JSONResponse({"id": id}, status_code=201, headers={'Location': f'{api_base_url}/users/{id}'})
except IntegrityError:
return JSONResponse({"message": "A user with the provided details already exists."}, status_code=400)

async def put(self, id: str, body: dict):
async def put(self, token_info, id: str, body: dict):
if not self.cb.check_permissions(token_info["role"], 'user', 'edit'):
return JSONResponse({"message": "Permission denied"}, status_code=403)

await self.um.update_user(id, body['name'], body['email'])
return JSONResponse({"message": "User updated successfully"}, status_code=200)

async def delete(self, id: str):
async def delete(self, token_info, id: str):
if not self.cb.check_permissions(token_info["role"], 'user', 'delete'):
return JSONResponse({"message": "Permission denied"}, status_code=403)
await self.um.delete_user(id)
return Response(status_code=204)

async def search(self, filter: str = None, range: str = None, sort: str = None):
async def search(self, token_info, filter: str = None, range: str = None, sort: str = None):
if not self.cb.check_permissions(token_info["role"], 'user', 'list'):
return JSONResponse({"message": "Permission denied"}, status_code=403)
result = parse_pagination_params(filter, range, sort)
if isinstance(result, JSONResponse):
return result
Expand Down
4 changes: 2 additions & 2 deletions backend/managers/AuthManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ async def webauthn_login(self, challenge: str, email_id:str, response):
"exp": datetime.utcnow() + timedelta(days=1)
}
token = generate_jwt(payload)
return token

return token, user.role
async def verify_email(self, token: str):
async with db_session_context() as session:
user_id = verify_email_token(token)
Expand Down
29 changes: 25 additions & 4 deletions backend/managers/CasbinRoleManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ def init_casbin(self):

def add_default_rules(self):
default_rules = [
("user", "DEFAULT", "GET"),
("admin", "DEFAULT", "POST"),
("admin", "DEFAULT", "PUT"),
("admin", "DEFAULT", "DELETE")
("user", "DEFAULT", "list"),
("user", "DEFAULT", "show"),
("admin", "DEFAULT", "create"),
("admin", "DEFAULT", "edit"),
("admin", "DEFAULT", "delete")
]

for rule in default_rules:
Expand All @@ -38,3 +39,23 @@ def add_default_rules(self):

def get_enforcer(self):
return self.enforcer

def check_permissions(self, role, resource, resource_type):
return self.enforcer.enforce(role, resource, resource_type)

def get_permissions(self, role="user"):
return self.enforcer.get_implicit_permissions_for_user(role)

def create_resource_access(self, role):
permissions = self.get_permissions(role)

output = {}
for _, resource, action in permissions:
if resource not in output:
output[resource] = []

if action not in output[resource]:
output[resource].append(action)

return output

2 changes: 1 addition & 1 deletion backend/rbac_model.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ p = role, res_id, act
e = some(where (p.eft == allow))

[matchers]
m = (r.res_id == p.res_id && r.act == p.act && r.role == p.role) || (p.res_id == "DEFAULT" && r.act == p.act && r.role == p.role)
m = ((r.res_id == p.res_id || p.res_id == "DEFAULT") && r.act == p.act && g(r.role,p.role))

[role_definition]
g = _, _
57 changes: 51 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { authProvider } from "./authProvider";
import { CustomLayout } from './CustomLayout';
import Login from './Login';
import { VerifyEmail } from './VerifyEmail';
import { hasAccess, ResourcePermissions } from './utils/authUtils';

export const App = () => (
<Admin
Expand All @@ -27,12 +28,56 @@ export const App = () => (
layout={CustomLayout}
loginPage={Login}
>
<Resource name="assets" list={AssetList} create={AssetCreate} edit={AssetEdit} show={AssetShow} recordRepresentation='name' icon={DocIcon} />
<Resource name="users" list={UserList} create={UserCreate} edit={UserEdit} show={UserShow} recordRepresentation='name' icon={UserIcon} />
<Resource name="abilities" list={AbilityList} show={AbilityShow} recordRepresentation='id' icon={ExtensionIcon} />
<Resource name="resources" list={ChannelList} show={ChannelShow} recordRepresentation='id' icon={SyncAltIcon} />
<Resource name="downloads" list={DownloadsList} />
<Resource name="shares" list={ShareList} create={ShareCreate} edit={ShareEdit} show={ShareShow} recordRepresentation='id' icon={LinkIcon} />
{(permissions: ResourcePermissions) => (
<>
{hasAccess("assets", "list", permissions) ?
<Resource
name="assets"
list={AssetList}
create={hasAccess("assets", "create", permissions) ? AssetCreate : undefined}
edit={hasAccess("assets", "edit", permissions) ? AssetEdit : undefined}
show={hasAccess("assets", "show", permissions) ? AssetShow : undefined}
recordRepresentation='name'
icon={DocIcon} /> : null}
{hasAccess("users", "list", permissions) ?
<Resource
name="users"
list={UserList}
create={hasAccess("users", "create", permissions) ? UserCreate : undefined}
edit={hasAccess("users", "edit", permissions) ? UserEdit : undefined}
show={hasAccess("users", "show", permissions) ? UserShow : undefined}
recordRepresentation='name'
icon={UserIcon} /> : null}
{hasAccess("abilities", "list", permissions) ?
<Resource
name="abilities"
list={AbilityList}
show={hasAccess("abilities", "show", permissions) ? AbilityShow : undefined}
recordRepresentation='id'
icon={ExtensionIcon} /> : null}
{hasAccess("resources", "list", permissions) ?
<Resource
name="resources"
list={ChannelList}
show={hasAccess("abilities", "show", permissions) ? ChannelShow : undefined}
recordRepresentation='id'
icon={SyncAltIcon} /> : null}
{hasAccess("downloads", "list", permissions) ?
<Resource
name="downloads"
list={DownloadsList} /> : null}
{hasAccess("shares", "list", permissions) ?
<Resource
name="shares"
list={ShareList}
create={hasAccess("shares", "create", permissions) ? ShareCreate : undefined}
edit={hasAccess("shares", "edit", permissions) ? ShareEdit : undefined}
show={hasAccess("shares", "show", permissions) ? ShareShow : undefined}
recordRepresentation='id'
icon={LinkIcon} /> : null}
</>
)
}
<CustomRoutes noLayout>
<Route path='/verify-email/:token' element={<VerifyEmail />} />
</CustomRoutes>
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/authProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const authProvider: AuthProvider = {
const res = await authentication(email)
if (res.token) {
localStorage.setItem("token", res.token)
localStorage.setItem("permissions", JSON.stringify(res.permissions))
return Promise.resolve()
} else {
return { redirectTo: false, stayOnLogin: true };
Expand All @@ -26,7 +27,7 @@ export const authProvider: AuthProvider = {
// called when the user clicks on the logout button
logout: () => {
logout()
localStorage.removeItem("token")
localStorage.clear()
return Promise.resolve();
},
// called when the API returns an error
Expand Down Expand Up @@ -56,7 +57,7 @@ export const authProvider: AuthProvider = {
const token = localStorage.getItem("token");
if (!token) return Promise.reject();

const decodedToken = jwtDecode<CustomJwtPayload>(token);
return Promise.resolve(decodedToken.roles);
const permissions = localStorage.getItem("permissions") ? JSON.parse(localStorage.getItem("permissions")!) : []
return Promise.resolve(permissions);
},
};
10 changes: 6 additions & 4 deletions frontend/src/users.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMediaQuery, Theme } from "@mui/material";
import { Create, Edit, EditButton, DeleteButton, List, SimpleList, Show, ShowButton, SimpleForm, SimpleShowLayout, Datagrid, TextField, TextInput, EmailField, SelectInput, useRecordContext } from "react-admin";
import { Create, Edit, EditButton, DeleteButton, List, SimpleList, Show, ShowButton, SimpleForm, SimpleShowLayout, Datagrid, TextField, TextInput, EmailField, SelectInput, useRecordContext, usePermissions } from "react-admin";
import { hasAccess } from "./utils/authUtils";

const UserTitle = () => {
const record = useRecordContext();
Expand All @@ -13,6 +14,7 @@ const roleChoices = [

export const UserList = () => {
const isSmall = useMediaQuery<Theme>((theme) => theme.breakpoints.down("sm"));
const { permissions } = usePermissions()
return (
<List>
{isSmall ? (
Expand All @@ -26,9 +28,9 @@ export const UserList = () => {
<TextField source="name" />
<EmailField source="email" />
<TextField source="role" />
<ShowButton />
<EditButton />
<DeleteButton />
{hasAccess("users", "show", permissions) && <ShowButton />}
{hasAccess("users", "edit", permissions) && <EditButton />}
{hasAccess("users", "delete", permissions) && <DeleteButton />}
</Datagrid>
)}
</List>
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/utils/authUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type ResourcePermissions = {
[key: string]: string[];
};

export const hasAccess = (
resourceId: string,
action: string,
permissions: ResourcePermissions
) => {
const resource = permissions[resourceId] || permissions["DEFAULT"];
if (!resource) return false;
const hasAccess = resource.includes(action);
return hasAccess;
};

0 comments on commit a3d410d

Please sign in to comment.