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:
|
start-dev:
|
||||||
docker compose -f docker/compose/docker-compose.yml up
|
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
|
.PHONY: build dev
|
||||||
build-dev:
|
build-dev:
|
||||||
docker compose -f docker/compose/docker-compose.yml build
|
docker compose -f docker/compose/docker-compose.yml build
|
||||||
|
|||||||
@@ -22,3 +22,12 @@ def create_user(db: Session, user: schemas.UserCreate):
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_user)
|
db.refresh(db_user)
|
||||||
return 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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from apps.base.models import Model
|
from apps.base.models import Model
|
||||||
|
from apps.users.roles import Role
|
||||||
|
|
||||||
|
|
||||||
class User(Model):
|
class User(Model):
|
||||||
@@ -10,4 +11,5 @@ class User(Model):
|
|||||||
email = Column(String, unique=True, index=True)
|
email = Column(String, unique=True, index=True)
|
||||||
hashed_password = Column(String)
|
hashed_password = Column(String)
|
||||||
is_active = Column(Boolean, default=True)
|
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.base.schemas import Schema
|
||||||
|
from apps.users.roles import Role
|
||||||
|
|
||||||
|
|
||||||
class UserBase(Schema):
|
class UserBase(Schema):
|
||||||
@@ -12,6 +13,7 @@ class UserCreate(UserBase):
|
|||||||
class User(UserBase):
|
class User(UserBase):
|
||||||
id: int
|
id: int
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
role: Role
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -28,3 +30,7 @@ class TokenData(Schema):
|
|||||||
|
|
||||||
class UserInDB(User):
|
class UserInDB(User):
|
||||||
hashed_password: str
|
hashed_password: str
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUserCreate(UserCreate):
|
||||||
|
role: Role
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Annotated
|
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 fastapi.security import OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from apps.users.crud import get_user_by_email
|
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 apps.users.schemas import TokenData, User, UserInDB
|
||||||
from middleware.db_connection import get_db
|
from middleware.db_connection import get_db
|
||||||
from settings import config
|
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)):
|
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)):
|
||||||
print(token)
|
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Could not validate credentials",
|
detail="Could not validate credentials",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
print(token)
|
||||||
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
print(payload)
|
print("payload", payload)
|
||||||
username: str = payload.get("sub")
|
username: str = payload.get("sub")
|
||||||
if username is None:
|
if username is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
token_data = TokenData(username=username)
|
token_data = TokenData(username=username)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
print(token_data.username)
|
||||||
user = get_user_by_email(db, email=token_data.username)
|
user = get_user_by_email(db, email=token_data.username)
|
||||||
|
print(user)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
return user
|
return user
|
||||||
@@ -91,3 +96,44 @@ async def get_current_active_user(
|
|||||||
if not current_user.is_active:
|
if not current_user.is_active:
|
||||||
raise HTTPException(status_code=400, detail="Inactive user")
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
return current_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
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
||||||
from starlette.staticfiles import StaticFiles
|
|
||||||
from fastapi.responses import FileResponse
|
from apps.users.roles import Role
|
||||||
from database import BaseModel, engine
|
from apps.users.schemas import UserCreate, User, AdminUserCreate
|
||||||
|
from database import BaseModel, engine, SessionLocal
|
||||||
from middleware.db_connection import DatabaseSessionMiddleware
|
from middleware.db_connection import DatabaseSessionMiddleware
|
||||||
|
|
||||||
from routes.main import api_router
|
from routes.main import api_router
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
from settings import config
|
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
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
print("Initialising database...")
|
print("Initialising database...")
|
||||||
BaseModel.metadata.create_all(bind=engine)
|
BaseModel.metadata.create_all(bind=engine)
|
||||||
print("Database initialised!")
|
print("Database initialised!")
|
||||||
|
# Initialize default users
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
init_default_users(db)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
print("App shutdown!")
|
print("App shutdown!")
|
||||||
|
|
||||||
@@ -27,8 +41,8 @@ middlewares = []
|
|||||||
|
|
||||||
app = FastAPI(lifespan=lifespan, middleware=middlewares)
|
app = FastAPI(lifespan=lifespan, middleware=middlewares)
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(DatabaseSessionMiddleware)
|
app.add_middleware(DatabaseSessionMiddleware)
|
||||||
app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.ALLOWED_HOSTS)
|
app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.ALLOWED_HOSTS)
|
||||||
|
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ fastapi[all]
|
|||||||
fastapi-utilities
|
fastapi-utilities
|
||||||
pydantic==2.6.0
|
pydantic==2.6.0
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
|
alembic
|
||||||
# deployment libraries
|
# deployment libraries
|
||||||
uvicorn==0.27.0
|
uvicorn==0.27.0
|
||||||
gunicorn
|
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 fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Annotated
|
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 apps.users.services import authenticate_user, fake_users_db, create_access_token, get_current_active_user
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
|
||||||
@@ -16,7 +18,8 @@ router = APIRouter(prefix="/users", tags=["users"])
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/token", response_model=Token)
|
@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)
|
user = authenticate_user(db, form_data.username, form_data.password)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -46,3 +49,5 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
|||||||
if db_user:
|
if db_user:
|
||||||
raise HTTPException(status_code=400, detail="Email already registered")
|
raise HTTPException(status_code=400, detail="Email already registered")
|
||||||
return crud.create_user(db=db, user=user)
|
return crud.create_user(db=db, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
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 = APIRouter(prefix="/api", tags=["api"])
|
||||||
api_router.include_router(welcome.router)
|
api_router.include_router(welcome.router)
|
||||||
api_router.include_router(user.router)
|
api_router.include_router(user.router)
|
||||||
|
api_router.include_router(agent.router)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user