From 7354b9bab82ddb05229d253a6f4689d36234d808 Mon Sep 17 00:00:00 2001 From: phil Date: Fri, 20 Dec 2024 11:45:44 +0100 Subject: [PATCH] Initdb: create database schemas, admin role, initial background map --- Containerfile.database | 3 -- database-container-entrypoint-postgis.sh | 29 ---------------- src/gisaf/api/main.py | 2 +- src/gisaf/database.py | 43 ++++++++++++++++++------ src/gisaf/models/map_bases.py | 6 ++-- src/gisaf/security.py | 40 +++++++++++++++++++++- 6 files changed, 76 insertions(+), 47 deletions(-) delete mode 100644 database-container-entrypoint-postgis.sh diff --git a/Containerfile.database b/Containerfile.database index 349bc18..291b2d9 100644 --- a/Containerfile.database +++ b/Containerfile.database @@ -2,6 +2,3 @@ FROM docker.io/postgis/postgis:17-3.5-alpine ENV POSTGRES_USER gisaf ENV POSTGRES_PASSWORD secret - -# Overwrite standard postgis entrypoint -COPY ./database-container-entrypoint-postgis.sh /docker-entrypoint-initdb.d/10_postgis.sh diff --git a/database-container-entrypoint-postgis.sh b/database-container-entrypoint-postgis.sh deleted file mode 100644 index 093241e..0000000 --- a/database-container-entrypoint-postgis.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -set -e - -# Perform all actions as $POSTGRES_USER -export PGUSER="$POSTGRES_USER" - -# Create the 'template_postgis' template db -"${psql[@]}" <<-'EOSQL' -CREATE DATABASE template_postgis IS_TEMPLATE true; -EOSQL - -# Load PostGIS into both template_database and $POSTGRES_DB -for DB in template_postgis "$POSTGRES_DB"; do - echo "Loading PostGIS extensions into $DB" - "${psql[@]}" --dbname="$DB" <<-'EOSQL' - CREATE EXTENSION IF NOT EXISTS postgis; -EOSQL -done - -"${psql[@]}" --dbname="$DB" <<-'EOSQL' - CREATE EXTENSION IF NOT EXISTS hstore; - CREATE SCHEMA gisaf; - CREATE SCHEMA gisaf_admin; - CREATE SCHEMA gisaf_map; - CREATE SCHEMA gisaf_survey; - CREATE SCHEMA raw_survey; - CREATE SCHEMA survey; -EOSQL diff --git a/src/gisaf/api/main.py b/src/gisaf/api/main.py index 05e30a8..b39a6da 100644 --- a/src/gisaf/api/main.py +++ b/src/gisaf/api/main.py @@ -115,7 +115,7 @@ async def get_acls( db_session: db_session, user: Annotated[User, Depends(get_current_active_user)] ) -> list[UserRoleLink]: """New: ACLs returned as UserRoleLink""" - if user is not None or not user.has_role("manager"): + if user is None or not user.has_role("manager"): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) data = await db_session.exec(select(UserRoleLink)) return data.all() # type: ignore[return-value] diff --git a/src/gisaf/database.py b/src/gisaf/database.py index 051d8a7..aa4dd63 100644 --- a/src/gisaf/database.py +++ b/src/gisaf/database.py @@ -5,7 +5,7 @@ from asyncio import sleep import logging from sqlalchemy.ext.asyncio import create_async_engine -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.orm import joinedload, QueryableAttribute from sqlalchemy.sql.selectable import Select from sqlmodel import SQLModel, select, func, col @@ -163,6 +163,8 @@ async def create_db(drop=False): attempts = CREATE_DB_TIMEOUT async def try_once(): + async with engine.begin() as conn: + await create_schemas(conn) async with engine.begin() as conn: if drop: await conn.run_sync(SQLModel.metadata.drop_all) @@ -199,14 +201,30 @@ async def is_fresh_install() -> bool: return nb_users == 0 +async def create_schemas(conn): + """Create schemas and extensions""" + raw_sql = [ + "CREATE EXTENSION IF NOT EXISTS hstore", + "CREATE SCHEMA IF NOT EXISTS gisaf", + "CREATE SCHEMA IF NOT EXISTS gisaf_admin", + "CREATE SCHEMA IF NOT EXISTS gisaf_map", + "CREATE SCHEMA IF NOT EXISTS gisaf_survey", + "CREATE SCHEMA IF NOT EXISTS raw_survey", + "CREATE SCHEMA IF NOT EXISTS survey", + ] + for rs in raw_sql: + await conn.execute(text(rs)) + + async def populate_init_db(): """Populate the database for a fresh install""" - from sqlalchemy import text - from gisaf.security import create_user # , add_role, add_user_role + from gisaf.security import create_user, create_role, add_user_role + from gisaf.models.map_bases import BaseStyle logger.info("Populating initial database") async with db_session() as session: + user = await create_user( session=session, username="admin", @@ -215,10 +233,15 @@ async def populate_init_db(): email="root@localhost.localdomain", active=True, ) - assert user is not None - # role = await add_role(role_id="admin") - # await add_user_role(user.username, role.name) - # for initial in initials: - # await session.execute(text(initial)) - # logger.debug(f"Added map style {initial}") - # await session.commit() + role = await create_role( + session, name="admin", description="Initial admin user" + ) + await session.refresh(user) + await session.refresh(role) + await add_user_role(session, user, role) + openFreeMap = BaseStyle( + name="OpenFreeMap", + static_url="https://tiles.openfreemap.org/styles/liberty", + ) + session.add(openFreeMap) + await session.commit() diff --git a/src/gisaf/models/map_bases.py b/src/gisaf/models/map_bases.py index d86e05a..972c59b 100644 --- a/src/gisaf/models/map_bases.py +++ b/src/gisaf/models/map_bases.py @@ -19,9 +19,9 @@ class BaseStyle(Model, table=True): id: int | None = Field(primary_key=True, default=None) name: str - style: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore - mbtiles: str = Field(sa_type=String(50)) # type: ignore - static_tiles_url: str + style: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True), default=None) + mbtiles: str | None = Field(sa_type=String(50), default=None) + static_url: str | None = Field(sa_type=String(250), default=None) enabled: bool = True def __repr__(self): diff --git a/src/gisaf/security.py b/src/gisaf/security.py index 11169fe..e830529 100644 --- a/src/gisaf/security.py +++ b/src/gisaf/security.py @@ -15,7 +15,7 @@ from sqlalchemy.orm import selectinload from gisaf.config import conf from gisaf.database import db_session -from gisaf.models.authentication import User, UserRead +from gisaf.models.authentication import User, UserRead, Role, UserRoleLink logger = logging.getLogger(__name__) @@ -97,6 +97,38 @@ async def create_user( return user_in_db +async def create_role( + session: AsyncSession, + name: str, + description: str | None = None, +): + role_in_db = await get_role(session, name) + if role_in_db is None: + role = Role( + name=name, + description=description, + ) + session.add(role) + await session.commit() + return role + else: + role_in_db.description = description + await session.commit() + return role_in_db + + +async def add_user_role(session: AsyncSession, user: User, role: Role) -> User | None: + query = select(UserRoleLink).where( + UserRoleLink.user_id == user.id, UserRoleLink.role_id == role.id + ) + data = await session.exec(query) + user_role = data.one_or_none() + if user_role is None: + user_role = UserRoleLink(user_id=user.id, role_id=role.id) + session.add(user_role) + await session.commit() + + async def get_user(session: AsyncSession, username: str) -> User | None: query = ( select(User).where(User.username == username).options(selectinload(User.roles)) @@ -105,6 +137,12 @@ async def get_user(session: AsyncSession, username: str) -> User | None: return data.one_or_none() +async def get_role(session: AsyncSession, name: str) -> Role | None: + query = select(Role).where(Role.name == name) + data = await session.exec(query) + return data.one_or_none() + + def verify_password(user: User, plain_password): try: return pwd_context.verify(plain_password, user.password)