mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on RBAC.
This commit is contained in:
@@ -2,6 +2,14 @@
|
||||
start-dev:
|
||||
docker compose -f docker/compose/docker-compose.yml up
|
||||
|
||||
.PHONY: down dev volumes
|
||||
down-dev-volumes:
|
||||
docker compose -f docker/compose/docker-compose.yml down --volumes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.PHONY: build dev
|
||||
build-dev:
|
||||
docker compose -f docker/compose/docker-compose.yml build
|
||||
|
||||
@@ -22,3 +22,12 @@ def create_user(db: Session, user: schemas.UserCreate):
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
|
||||
def create_admin_user(db: Session, user: schemas.AdminUserCreate):
|
||||
fake_hashed_password = services.get_password_hash(user.password)
|
||||
db_user = models.User(email=user.email, hashed_password=fake_hashed_password, role=user.role)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from sqlalchemy import Column, Boolean, String, Float
|
||||
from sqlalchemy import Column, Boolean, String, Float, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from apps.base.models import Model
|
||||
from apps.users.roles import Role
|
||||
|
||||
|
||||
class User(Model):
|
||||
@@ -10,4 +11,5 @@ class User(Model):
|
||||
email = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
role = Column(Enum(Role), default=Role.USER) # Add a role attribute
|
||||
# role = Column(String) # Ensure role is defined
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Role(str, Enum):
|
||||
ADMIN = "admin"
|
||||
MODERATOR = "moderator"
|
||||
USER = "user"
|
||||
|
||||
|
||||
class Permission(str, Enum):
|
||||
CREATE = "create"
|
||||
READ = "read"
|
||||
UPDATE = "update"
|
||||
DELETE = "delete"
|
||||
|
||||
|
||||
role_permissions = {
|
||||
Role.ADMIN: [Permission.CREATE, Permission.READ, Permission.UPDATE, Permission.DELETE],
|
||||
Role.MODERATOR: [Permission.CREATE, Permission.READ, Permission.UPDATE],
|
||||
Role.USER: [Permission.READ]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
from apps.base.schemas import Schema
|
||||
from apps.users.roles import Role
|
||||
|
||||
|
||||
class UserBase(Schema):
|
||||
@@ -12,6 +13,7 @@ class UserCreate(UserBase):
|
||||
class User(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
role: Role
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -28,3 +30,7 @@ class TokenData(Schema):
|
||||
|
||||
class UserInDB(User):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class AdminUserCreate(UserCreate):
|
||||
role: Role
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
from fastapi import Request
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, status, Security
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy.orm import Session
|
||||
from functools import wraps
|
||||
|
||||
from apps.users.crud import get_user_by_email
|
||||
from apps.users.roles import Permission, role_permissions
|
||||
from apps.users.schemas import TokenData, User, UserInDB
|
||||
from middleware.db_connection import get_db
|
||||
from settings import config
|
||||
@@ -64,22 +67,24 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
|
||||
|
||||
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)):
|
||||
print(token)
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
print(token)
|
||||
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
|
||||
print(payload)
|
||||
print("payload", payload)
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
print(token_data.username)
|
||||
user = get_user_by_email(db, email=token_data.username)
|
||||
print(user)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
@@ -91,3 +96,44 @@ async def get_current_active_user(
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
def permission_required(permission: Permission):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def decorated_function(*args, **kwargs):
|
||||
print("test", kwargs)
|
||||
request: Request = kwargs.get("request")
|
||||
db: Session = kwargs.get("db")
|
||||
if not request:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Missing request object",
|
||||
)
|
||||
|
||||
# Get the authorization header
|
||||
authorization_header = request.headers.get("Authorization")
|
||||
|
||||
# Extract the token from the authorization header
|
||||
if not authorization_header:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authorization header",
|
||||
)
|
||||
|
||||
# Assuming the token is in the format "Bearer <token>"
|
||||
token = authorization_header.split(" ")[1]
|
||||
|
||||
current_user = await get_current_user(token=token, db=db)
|
||||
# Check if the user has the required permission
|
||||
if permission not in role_permissions[current_user.role]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You do not have the required permission",
|
||||
)
|
||||
# Call the decorated function with the original arguments
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
+22
-8
@@ -1,24 +1,38 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from database import BaseModel, engine
|
||||
|
||||
from apps.users.roles import Role
|
||||
from apps.users.schemas import UserCreate, User, AdminUserCreate
|
||||
from database import BaseModel, engine, SessionLocal
|
||||
from middleware.db_connection import DatabaseSessionMiddleware
|
||||
|
||||
from routes.main import api_router
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from settings import config
|
||||
|
||||
|
||||
def init_default_users(db: Session):
|
||||
from apps.users import crud
|
||||
print("Initializing default users...")
|
||||
if not crud.get_user_by_email(db, "admin@portabase.com"):
|
||||
print("Creating admin user...")
|
||||
user = AdminUserCreate(email="admin@portabase.com", password="password", role=Role.ADMIN)
|
||||
crud.create_admin_user(db, user)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
print("Initialising database...")
|
||||
BaseModel.metadata.create_all(bind=engine)
|
||||
print("Database initialised!")
|
||||
# Initialize default users
|
||||
db = SessionLocal()
|
||||
try:
|
||||
init_default_users(db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
yield
|
||||
print("App shutdown!")
|
||||
|
||||
@@ -27,8 +41,8 @@ middlewares = []
|
||||
|
||||
app = FastAPI(lifespan=lifespan, middleware=middlewares)
|
||||
|
||||
|
||||
app.add_middleware(DatabaseSessionMiddleware)
|
||||
app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.ALLOWED_HOSTS)
|
||||
|
||||
app.include_router(api_router)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ fastapi[all]
|
||||
fastapi-utilities
|
||||
pydantic==2.6.0
|
||||
pydantic-settings
|
||||
|
||||
alembic
|
||||
# deployment libraries
|
||||
uvicorn==0.27.0
|
||||
gunicorn
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from fastapi import Depends, APIRouter, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from apps.users.roles import Permission
|
||||
from apps.users.services import permission_required, get_current_user, get_current_active_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
from middleware.db_connection import get_db
|
||||
|
||||
router = APIRouter(prefix="/agents", tags=["agents"])
|
||||
|
||||
|
||||
class Agent(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
# Example usage:
|
||||
@router.post("/", dependencies=[Depends(get_current_active_user)])
|
||||
@permission_required(Permission.CREATE)
|
||||
async def create_agent(agent: Agent, request: Request, db: Session = Depends(get_db)):
|
||||
print(agent)
|
||||
return True
|
||||
@@ -3,6 +3,8 @@ from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Annotated
|
||||
|
||||
from apps.users.roles import Permission
|
||||
from apps.users.services import authenticate_user, fake_users_db, create_access_token, get_current_active_user
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
@@ -16,7 +18,8 @@ router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Session = Depends(get_db)):
|
||||
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
db: Session = Depends(get_db)):
|
||||
user = authenticate_user(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
@@ -46,3 +49,5 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
return crud.create_user(db=db, user=user)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from routes.http import welcome, user
|
||||
from routes.http import welcome, user, agent
|
||||
|
||||
api_router = APIRouter(prefix="/api", tags=["api"])
|
||||
api_router.include_router(welcome.router)
|
||||
api_router.include_router(user.router)
|
||||
api_router.include_router(agent.router)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user