diff --git a/.env.dev b/.env.dev index db6b050..b6325da 100644 --- a/.env.dev +++ b/.env.dev @@ -1,13 +1,13 @@ -CGNO_ID_JWT_SECRET=secret +JWT_SECRET=secret # Database -CGNO_ID_DB_USERNAME=root -CGNO_ID_DB_PASSWORD=root -CGNO_ID_DB_HOST=127.0.0.1 -CGNO_ID_DB_NAME=cgno-id +DB_USERNAME=root +DB_PASSWORD=root +DB_HOST=127.0.0.1 +DB_NAME=cgno-id # # Root -CGNO_ROOT_EMAIL=cognioandrey@gmail.com -CGNO_ROOT_PASSWORD=password +ROOT_EMAIL=cognioandrey@gmail.com +ROOT_PASSWORD=password # \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..f5b1058 Binary files /dev/null and b/app/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/__pycache__/config.cpython-39.pyc b/app/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000..2346acc Binary files /dev/null and b/app/__pycache__/config.cpython-39.pyc differ diff --git a/app/__pycache__/db.cpython-39.pyc b/app/__pycache__/db.cpython-39.pyc new file mode 100644 index 0000000..6efa9b9 Binary files /dev/null and b/app/__pycache__/db.cpython-39.pyc differ diff --git a/app/__pycache__/init.cpython-39.pyc b/app/__pycache__/init.cpython-39.pyc new file mode 100644 index 0000000..b799c9c Binary files /dev/null and b/app/__pycache__/init.cpython-39.pyc differ diff --git a/app/__pycache__/main.cpython-39.pyc b/app/__pycache__/main.cpython-39.pyc new file mode 100644 index 0000000..114f6e1 Binary files /dev/null and b/app/__pycache__/main.cpython-39.pyc differ diff --git a/app/access_control/__init__.py b/app/access_control/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/access_control/__pycache__/__init__.cpython-39.pyc b/app/access_control/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..f0c600e Binary files /dev/null and b/app/access_control/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/access_control/__pycache__/role.cpython-39.pyc b/app/access_control/__pycache__/role.cpython-39.pyc new file mode 100644 index 0000000..79cc910 Binary files /dev/null and b/app/access_control/__pycache__/role.cpython-39.pyc differ diff --git a/app/access_control/role.py b/app/access_control/role.py new file mode 100644 index 0000000..935789b --- /dev/null +++ b/app/access_control/role.py @@ -0,0 +1,6 @@ +from app.user.model import User + + +def has_role(name: str, user: User): + if user.role.name != name: + raise Exception() diff --git a/app/auth/__pycache__/__init__.cpython-39.pyc b/app/auth/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..ef9f2f6 Binary files /dev/null and b/app/auth/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/auth/__pycache__/dto.cpython-39.pyc b/app/auth/__pycache__/dto.cpython-39.pyc new file mode 100644 index 0000000..b625663 Binary files /dev/null and b/app/auth/__pycache__/dto.cpython-39.pyc differ diff --git a/app/auth/__pycache__/handlers.cpython-39.pyc b/app/auth/__pycache__/handlers.cpython-39.pyc new file mode 100644 index 0000000..4233117 Binary files /dev/null and b/app/auth/__pycache__/handlers.cpython-39.pyc differ diff --git a/app/auth/__pycache__/middleware.cpython-39.pyc b/app/auth/__pycache__/middleware.cpython-39.pyc new file mode 100644 index 0000000..4fb1775 Binary files /dev/null and b/app/auth/__pycache__/middleware.cpython-39.pyc differ diff --git a/app/auth/__pycache__/service.cpython-39.pyc b/app/auth/__pycache__/service.cpython-39.pyc new file mode 100644 index 0000000..e270c55 Binary files /dev/null and b/app/auth/__pycache__/service.cpython-39.pyc differ diff --git a/app/auth/service.py b/app/auth/service.py index f1ab419..b324b18 100644 --- a/app/auth/service.py +++ b/app/auth/service.py @@ -10,8 +10,8 @@ from app.auth.dto import Credentials from app.config import config from app.user.service import get_user_by_username, passwords_match -JWT_SECRET = config["CGNO_ID_JWT_SECRET"] -JWT_ISSUER = "Energia" +JWT_SECRET = config["JWT_SECRET"] +JWT_ISSUER = "cognio ID" def authenticate(credentials: Credentials, db: Session) -> Optional[str]: diff --git a/app/db.py b/app/db.py index d14febc..6c425e2 100644 --- a/app/db.py +++ b/app/db.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import sessionmaker from app.config import config -DATABASE_URL = f"postgresql://{config['CGNO_ID_DB_USERNAME']}:{config['CGNO_ID_DB_PASSWORD']}@{config['CGNO_ID_DB_HOST']}/{config['CGNO_ID_DB_NAME']}" +DATABASE_URL = f"postgresql://{config['DB_USERNAME']}:{config['DB_PASSWORD']}@{config['DB_HOST']}/{config['DB_NAME']}" engine = create_engine(DATABASE_URL) diff --git a/app/init.py b/app/init.py new file mode 100644 index 0000000..8e6d094 --- /dev/null +++ b/app/init.py @@ -0,0 +1,23 @@ +from app.config import config +from app.db import session_factory +from app.role.service import create_admin_role +from app.user.service import create_root_user + + +def init_root_user(): + db = session_factory() + + root_email = config["ROOT_EMAIL"] + root_password = config["ROOT_PASSWORD"] + + create_root_user(db, root_email, root_password) + + db.close() + + +def init_admin_role(): + db = session_factory() + + create_admin_role(db) + + db.close() diff --git a/app/main.py b/app/main.py index 385045f..f68138b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,10 @@ +import uvicorn from fastapi import FastAPI, APIRouter from starlette.middleware.cors import CORSMiddleware from app.user.handlers import router as user_router from app.auth.handlers import router as auth_router -from app.user.root import init_root_user +from app.init import init_root_user, init_admin_role def main_router() -> APIRouter: @@ -15,6 +16,7 @@ def main_router() -> APIRouter: app = FastAPI(title="cognio ID API") +app.add_event_handler("startup", init_admin_role) app.add_event_handler("startup", init_root_user) app.add_middleware( CORSMiddleware, @@ -25,3 +27,6 @@ app.add_middleware( ) app.include_router(main_router()) + +if __name__ == "__main__": + uvicorn.run("app.main:app") diff --git a/app/migrations/__pycache__/env.cpython-39.pyc b/app/migrations/__pycache__/env.cpython-39.pyc new file mode 100644 index 0000000..c9d0d0b Binary files /dev/null and b/app/migrations/__pycache__/env.cpython-39.pyc differ diff --git a/app/migrations/env.py b/app/migrations/env.py index ef42324..613c16f 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -9,8 +9,9 @@ from alembic import context sys.path.append(str(pathlib.Path(__file__).resolve().parents[2])) -from app.db import DATABASE_URL +from app.db import DATABASE_URL, EntityBase from app.user.model import User +from app.role.model import Role # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -24,7 +25,7 @@ fileConfig(config.config_file_name) # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = [User.metadata] +target_metadata = EntityBase.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/app/migrations/versions/2a84260bc774_add_roles.py b/app/migrations/versions/2a84260bc774_add_roles.py new file mode 100644 index 0000000..95b3d6e --- /dev/null +++ b/app/migrations/versions/2a84260bc774_add_roles.py @@ -0,0 +1,39 @@ +"""Add roles + +Revision ID: 2a84260bc774 +Revises: 646ae6f3e17a +Create Date: 2021-02-28 23:06:29.207602 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2a84260bc774' +down_revision = '646ae6f3e17a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('roles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('creation_date', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.add_column('users', sa.Column('role_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'users', type_='foreignkey') + op.drop_column('users', 'role_id') + op.drop_table('roles') + # ### end Alembic commands ### diff --git a/app/migrations/versions/__pycache__/2a84260bc774_add_roles.cpython-39.pyc b/app/migrations/versions/__pycache__/2a84260bc774_add_roles.cpython-39.pyc new file mode 100644 index 0000000..4482a95 Binary files /dev/null and b/app/migrations/versions/__pycache__/2a84260bc774_add_roles.cpython-39.pyc differ diff --git a/app/migrations/versions/__pycache__/646ae6f3e17a_initial.cpython-39.pyc b/app/migrations/versions/__pycache__/646ae6f3e17a_initial.cpython-39.pyc new file mode 100644 index 0000000..77a1f4b Binary files /dev/null and b/app/migrations/versions/__pycache__/646ae6f3e17a_initial.cpython-39.pyc differ diff --git a/app/role/__init__.py b/app/role/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/role/__pycache__/__init__.cpython-39.pyc b/app/role/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..f7c0f05 Binary files /dev/null and b/app/role/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/role/__pycache__/model.cpython-39.pyc b/app/role/__pycache__/model.cpython-39.pyc new file mode 100644 index 0000000..e0890e2 Binary files /dev/null and b/app/role/__pycache__/model.cpython-39.pyc differ diff --git a/app/role/__pycache__/service.cpython-39.pyc b/app/role/__pycache__/service.cpython-39.pyc new file mode 100644 index 0000000..b75ddf5 Binary files /dev/null and b/app/role/__pycache__/service.cpython-39.pyc differ diff --git a/app/role/model.py b/app/role/model.py new file mode 100644 index 0000000..2aacdb8 --- /dev/null +++ b/app/role/model.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from sqlalchemy import Column, Integer, String, DateTime, Text + +from app.db import EntityBase + + +class Role(EntityBase): + __tablename__ = "roles" + + id = Column(Integer, primary_key=True) + name = Column(String(64), unique=True, nullable=False) + description = Column(Text) + creation_date = Column(DateTime, nullable=False, default=datetime.utcnow()) diff --git a/app/role/service.py b/app/role/service.py new file mode 100644 index 0000000..e2d46aa --- /dev/null +++ b/app/role/service.py @@ -0,0 +1,49 @@ +from typing import Optional + +from sqlalchemy.orm import Session + +from app.role.model import Role + + +def create_role(db: Session, role: Role) -> Role: + role.name = role.name.lower() + + if get_role_by_name(db, role.name) is not None: + raise Exception() + + db.add(role) + db.commit() + db.refresh(role) + + return role + + +def get_role_by_name(db: Session, name: str) -> Optional[Role]: + return db.query(Role).filter(Role.name == name).one_or_none() + + +def get_role_by_id(db: Session, id: int) -> Optional[Role]: + return db.query(Role).filter(Role.id == id).one_or_none() + + +def get_roles(db: Session) -> list[Role]: + return db.query(Role).all() + + +def create_admin_role(db: Session): + admin_role = get_role_by_name(db, "admin") + + if admin_role is None: + db.add(Role(name="admin")) + db.commit() + + +def delete_role_by_id(db: Session, id: int): + role = get_role_by_id(db, id) + + if role.name == "admin": + return + + if role is not None: + db.delete(role) + db.commit() diff --git a/app/user/__pycache__/__init__.cpython-39.pyc b/app/user/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..ff3a525 Binary files /dev/null and b/app/user/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/user/__pycache__/dto.cpython-39.pyc b/app/user/__pycache__/dto.cpython-39.pyc new file mode 100644 index 0000000..ca2f090 Binary files /dev/null and b/app/user/__pycache__/dto.cpython-39.pyc differ diff --git a/app/user/__pycache__/handlers.cpython-39.pyc b/app/user/__pycache__/handlers.cpython-39.pyc new file mode 100644 index 0000000..c3568e5 Binary files /dev/null and b/app/user/__pycache__/handlers.cpython-39.pyc differ diff --git a/app/user/__pycache__/model.cpython-39.pyc b/app/user/__pycache__/model.cpython-39.pyc new file mode 100644 index 0000000..2e04c67 Binary files /dev/null and b/app/user/__pycache__/model.cpython-39.pyc differ diff --git a/app/user/__pycache__/service.cpython-39.pyc b/app/user/__pycache__/service.cpython-39.pyc new file mode 100644 index 0000000..afeb3fa Binary files /dev/null and b/app/user/__pycache__/service.cpython-39.pyc differ diff --git a/app/user/handlers.py b/app/user/handlers.py index f26f57d..2eae158 100644 --- a/app/user/handlers.py +++ b/app/user/handlers.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from starlette.responses import Response +from app.access_control.role import has_role from app.auth.middleware import get_auth_user from app.user.dto import UserCreationModel, UserResourceModel from app.user.model import User @@ -21,6 +22,7 @@ async def create_user( ) -> UserResourceModel: user = model.to_entity() created_user = user_service.create_user(db, user) + return UserResourceModel.from_entity(created_user) diff --git a/app/user/model.py b/app/user/model.py index 0b1b62e..b69bafa 100644 --- a/app/user/model.py +++ b/app/user/model.py @@ -1,9 +1,11 @@ import enum from datetime import datetime -from sqlalchemy import Column, String, Integer, DateTime, Enum, Date +from sqlalchemy import Column, String, Integer, DateTime, Enum, Date, ForeignKey +from sqlalchemy.orm import relationship from app.db import EntityBase +from app.role.model import Role class Sex(enum.Enum): @@ -22,8 +24,14 @@ class User(EntityBase): username = Column(String(length=32), unique=True, nullable=False) email = Column(String, unique=True, nullable=False) password = Column(String, nullable=False) + role_id = Column(Integer, ForeignKey("roles.id")) given_name = Column(String(length=32), nullable=False) family_name = Column(String(length=32)) sex = Column(Enum(Sex), nullable=False, default=Sex.NOT_KNOWN) birthdate = Column(Date) creation_date = Column(DateTime, nullable=False, default=datetime.utcnow()) + + role = relationship("Role", back_populates="users") + + +Role.users = relationship("User", back_populates="role") diff --git a/app/user/root.py b/app/user/root.py deleted file mode 100644 index 050ff35..0000000 --- a/app/user/root.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import date - -from app.config import config -from app.db import session_factory -from app.user import service as user_service -from app.user.model import User - - -def init_root_user(): - db = session_factory() - root_user = user_service.get_user_by_username(db, "root") - - if root_user is None: - user_service.create_user(db, User( - username="root", - email=config["CGNO_ROOT_EMAIL"], - password=config["CGNO_ROOT_PASSWORD"], - given_name="Root", - birthdate=date.today() - )) - db.close() diff --git a/app/user/service.py b/app/user/service.py index 32dd92b..016983a 100644 --- a/app/user/service.py +++ b/app/user/service.py @@ -1,15 +1,18 @@ +from datetime import date from typing import Optional from passlib.context import CryptContext from sqlalchemy.orm import Session from app.user.model import User +from app.role import service as role_service pwd_context = CryptContext(schemes=["bcrypt"]) def create_user(db: Session, user: User) -> User: - if get_user_by_username_or_email(db, user.username, user.email) is not None: + existing_user = get_user_by_username_or_email(db, user.username, user.email) + if existing_user is not None: raise Exception() user.password = pwd_context.hash(user.password) @@ -31,8 +34,28 @@ def get_user_by_username_or_email(db: Session, username: str, email: str) -> Opt return db.query(User).filter(User.username == username, User.email == email).one_or_none() +def create_root_user(db: Session, root_email: str, root_password: str): + root_user = get_user_by_username(db, "root") + admin_role = role_service.get_role_by_name(db, "admin") + + if root_user is None: + root_user = User( + username="root", + email=root_email, + password=root_password, + given_name="Root", + role=admin_role, + birthdate=date.today() + ) + create_user(db, root_user) + + def delete_user_by_id(db: Session, id: int): user = get_user_by_id(db, id) + + if user.username == "root": + raise Exception() + if user is not None: db.delete(user) db.commit() diff --git a/docker-compose-dev-win.yml b/docker-compose-dev-win.yml index b902b28..b0d4d46 100644 --- a/docker-compose-dev-win.yml +++ b/docker-compose-dev-win.yml @@ -8,4 +8,4 @@ services: env_file: - ./.env.dev environment: - - CGNO_ID_DB_HOST=host.docker.internal + - DB_HOST=host.docker.internal