Skip to content

Commit

Permalink
feat!: Add support for global configuration
Browse files Browse the repository at this point in the history
The global configuration is now defined in the yaml format in the frontend.
We use the `monaco` editor.

This is a breaking change. The following configuration options
have to be configured manually after the update:
- privacy policy URL
- imprint URL
- provider
- authentication provider (Only the display name in the frontend)
- environment (Only the display name in the frontend)
  • Loading branch information
MoritzWeber0 committed Jan 30, 2024
1 parent c8fe2e4 commit 0b0eb42
Show file tree
Hide file tree
Showing 32 changed files with 9,091 additions and 8,680 deletions.
1 change: 0 additions & 1 deletion backend/capellacollab/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# SPDX-License-Identifier: Apache-2.0

import os
from logging.config import fileConfig

os.environ["ALEMBIC_CONTEXT"] = "1"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

"""Add configuration table
Revision ID: 86ab7d4d1684
Revises: f55b41e32223
Create Date: 2023-10-27 14:54:40.452599
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "86ab7d4d1684"
down_revision = "f55b41e32223"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"configuration",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column(
"configuration",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_configuration_id"), "configuration", ["id"], unique=True
)
op.create_index(
op.f("ix_configuration_name"), "configuration", ["name"], unique=True
)
13 changes: 0 additions & 13 deletions backend/capellacollab/config/config_schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,6 @@ properties:
type: string
wildcardHost:
type: boolean
metadata:
type: object
properties:
privacyPolicyURL:
type: string
imprintURL:
type: string
provider:
type: string
authenticationProvider:
type: string
environment:
type: string
extensions:
type: object
additionalProperties: false
Expand Down
5 changes: 4 additions & 1 deletion backend/capellacollab/core/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@


class Base(orm.DeclarativeBase):
type_annotation_map = {dict[str, str]: postgresql.JSONB}
type_annotation_map = {
dict[str, str]: postgresql.JSONB,
dict[str, t.Any]: postgresql.JSONB,
}


### SQL MODELS ARE IMPORTED HERE ###
Expand Down
1 change: 1 addition & 0 deletions backend/capellacollab/core/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import capellacollab.projects.toolmodels.restrictions.models
import capellacollab.projects.users.models
import capellacollab.sessions.models
import capellacollab.settings.configuration.models
import capellacollab.settings.integrations.purevariants.models
import capellacollab.settings.modelsources.git.models
import capellacollab.settings.modelsources.t4c.models
Expand Down
28 changes: 16 additions & 12 deletions backend/capellacollab/core/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@

import fastapi
import pydantic
from sqlalchemy import orm

import capellacollab
from capellacollab.config import config
from capellacollab.core import database
from capellacollab.settings.configuration import core as config_core
from capellacollab.settings.configuration import models as config_models


class Metadata(pydantic.BaseModel):
Expand All @@ -28,22 +32,22 @@ class Metadata(pydantic.BaseModel):
router = fastapi.APIRouter()

general_cfg: dict[str, t.Any] = config["general"]
metadata_cfg: dict[str, str | None] = general_cfg.get("metadata", {})


@router.get(
"/metadata",
response_model=Metadata,
)
def get_metadata():
return Metadata(
version=capellacollab.__version__,
privacy_policy_url=metadata_cfg.get("privacyPolicyURL"),
imprint_url=metadata_cfg.get("imprintURL"),
provider=metadata_cfg.get("provider"),
authentication_provider=metadata_cfg.get("authenticationProvider"),
environment=metadata_cfg.get("environment"),
host=general_cfg.get("host"),
port=str(general_cfg.get("port")),
protocol=general_cfg.get("scheme"),
def get_metadata(db: orm.Session = fastapi.Depends(database.get_db)):
cfg = config_core.get_config(db, "global")
assert isinstance(cfg, config_models.GlobalConfiguration)

Check warning on line 43 in backend/capellacollab/core/metadata.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/core/metadata.py#L42-L43

Added lines #L42 - L43 were not covered by tests

return Metadata.model_validate(

Check warning on line 45 in backend/capellacollab/core/metadata.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/core/metadata.py#L45

Added line #L45 was not covered by tests
cfg.metadata.model_dump()
| {
"version": capellacollab.__version__,
"host": general_cfg.get("host"),
"port": str(general_cfg.get("port")),
"protocol": general_cfg.get("scheme"),
}
)
1 change: 0 additions & 1 deletion backend/capellacollab/projects/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

from . import crud, models

logger = logging.getLogger(__name__)
router = fastapi.APIRouter(
dependencies=[
fastapi.Depends(
Expand Down
4 changes: 4 additions & 0 deletions backend/capellacollab/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
prefix="/settings",
responses=auth_responses.AUTHENTICATION_RESPONSES,
)
router.include_router(
settings_routes.router_without_authentication,
prefix="/settings",
)

# Load authentication routes
ep = authentication.get_authentication_entrypoint()
Expand Down
2 changes: 2 additions & 0 deletions backend/capellacollab/settings/configuration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
15 changes: 15 additions & 0 deletions backend/capellacollab/settings/configuration/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

from sqlalchemy import orm

from . import crud, models


def get_config(db: orm.Session, name: str) -> models.ConfigurationBase:
"""Get a configuration by name."""
configuration = crud.get_configuration_by_name(db, name)
model_type = models.NAME_TO_MODEL_TYPE_MAPPING[name]

Check warning on line 12 in backend/capellacollab/settings/configuration/core.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/core.py#L11-L12

Added lines #L11 - L12 were not covered by tests
if configuration:
return model_type().model_validate(configuration.configuration)
return model_type().model_validate({})

Check warning on line 15 in backend/capellacollab/settings/configuration/core.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/core.py#L14-L15

Added lines #L14 - L15 were not covered by tests
55 changes: 55 additions & 0 deletions backend/capellacollab/settings/configuration/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import sqlalchemy as sa
from sqlalchemy import orm

from . import models


def get_configuration_by_name(
db: orm.Session, name: str
) -> None | models.DatabaseConfiguration:
return db.execute(

Check warning on line 13 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L13

Added line #L13 was not covered by tests
sa.select(models.DatabaseConfiguration).where(
models.DatabaseConfiguration.name == name
)
).scalar_one_or_none()


def get_pydantic_configuration_by_name(
db: orm.Session, pydantic_model: models.ConfigurationBase
):
return pydantic_model.model_validate(

Check warning on line 23 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L23

Added line #L23 was not covered by tests
get_configuration_by_name(db, pydantic_model._name)
)


def create_configuration(
db: orm.Session,
name: str,
configuration: dict[str, str],
) -> models.DatabaseConfiguration:
db_configuration = models.DatabaseConfiguration(

Check warning on line 33 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L33

Added line #L33 was not covered by tests
name=name, configuration=configuration
)
db.add(db_configuration)
db.commit()
return db_configuration

Check warning on line 38 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L36-L38

Added lines #L36 - L38 were not covered by tests


def update_configuration(
db: orm.Session,
db_configuration: models.DatabaseConfiguration,
configuration: dict[str, str],
) -> models.DatabaseConfiguration:
db_configuration.configuration = configuration
db.commit()
return db_configuration

Check warning on line 48 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L46-L48

Added lines #L46 - L48 were not covered by tests


def delete_configuration(
db: orm.Session, db_configuration: models.DatabaseConfiguration
):
db.delete(db_configuration)
db.commit()

Check warning on line 55 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L54-L55

Added lines #L54 - L55 were not covered by tests
65 changes: 65 additions & 0 deletions backend/capellacollab/settings/configuration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import abc
import typing as t

import pydantic
from sqlalchemy import orm

from capellacollab.core import database


class DatabaseConfiguration(database.Base):
__tablename__ = "configuration"

id: orm.Mapped[int] = orm.mapped_column(
unique=True, primary_key=True, index=True
)

name: orm.Mapped[str] = orm.mapped_column(unique=True, index=True)
configuration: orm.Mapped[dict[str, t.Any]]


class MetadataConfiguration(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid")

privacy_policy_url: str = pydantic.Field(
default="https://example.com/privacy"
)
imprint_url: str = pydantic.Field(default="https://example.com/imprint")
provider: str = pydantic.Field(
default="Systems Engineering Toolchain team"
)
authentication_provider: str = pydantic.Field(
default="OAuth2",
description="Authentication provides which is displayed in the frontend.",
)
environment: str = pydantic.Field(default="-", description="general")


class ConfigurationBase(pydantic.BaseModel, abc.ABC):
"""
Base class for configuration models. Can be used to define new configurations
in the future.
"""

model_config = pydantic.ConfigDict(extra="forbid")

_name: t.ClassVar[str]


class GlobalConfiguration(ConfigurationBase):
"""Global application configuration."""

_name: t.ClassVar[t.Literal["global"]] = "global"

metadata: MetadataConfiguration = pydantic.Field(
default_factory=MetadataConfiguration
)


# All subclasses of ConfigurationBase are automatically registered using this dict.
NAME_TO_MODEL_TYPE_MAPPING: dict[str, t.Type[ConfigurationBase]] = {
model()._name: model for model in ConfigurationBase.__subclasses__()
}
63 changes: 63 additions & 0 deletions backend/capellacollab/settings/configuration/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import typing as t

import fastapi
from sqlalchemy import orm

from capellacollab.core import database
from capellacollab.core.authentication import injectables as auth_injectables
from capellacollab.users import models as users_models

from . import core, crud, models

router = fastapi.APIRouter(
dependencies=[
fastapi.Depends(
auth_injectables.RoleVerification(
required_role=users_models.Role.ADMIN
)
)
]
)

schema_router = fastapi.APIRouter(dependencies=[])


@router.get(
f"/{models.GlobalConfiguration._name}",
response_model=models.GlobalConfiguration,
)
async def get_configuration(
db: orm.Session = fastapi.Depends(database.get_db),
):
return core.get_config(db, models.GlobalConfiguration._name)

Check warning on line 35 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L35

Added line #L35 was not covered by tests


@router.put(
f"/{models.GlobalConfiguration._name}",
response_model=models.GlobalConfiguration,
)
async def update_configuration(
body: models.GlobalConfiguration,
db: orm.Session = fastapi.Depends(database.get_db),
):
configuration = crud.get_configuration_by_name(

Check warning on line 46 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L46

Added line #L46 was not covered by tests
db, models.GlobalConfiguration._name
)

if configuration:
return crud.update_configuration(db, configuration, body.model_dump())
return crud.create_configuration(

Check warning on line 52 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L51-L52

Added lines #L51 - L52 were not covered by tests
db,
name=models.GlobalConfiguration._name,
configuration=body.model_dump(),
)


@schema_router.get(
f"/{models.GlobalConfiguration._name}/schema", response_model=t.Any
)
async def get_json_schema():
return models.GlobalConfiguration.model_json_schema()

Check warning on line 63 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L63

Added line #L63 was not covered by tests
7 changes: 7 additions & 0 deletions backend/capellacollab/settings/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fastapi

from capellacollab.core.authentication import injectables as auth_injectables
from capellacollab.settings.configuration import routes as configuration_routes
from capellacollab.settings.integrations.purevariants import (
routes as purevariants_routes,
)
Expand All @@ -19,10 +20,16 @@
)
]
)
router_without_authentication = fastapi.APIRouter()

router.include_router(
modelsources_routes.router,
prefix="/modelsources",
)
router.include_router(
purevariants_routes.router, prefix="/integrations/pure-variants"
)
router.include_router(configuration_routes.router, prefix="/configurations")
router_without_authentication.include_router(
configuration_routes.schema_router, prefix="/configurations"
)
7 changes: 0 additions & 7 deletions backend/config/config_template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ general:
scheme: http
wildcardHost: False

metadata:
privacyPolicyURL: https://example.com/privacy
imprintURL: https://example.com/imprint
provider: Systems Engineering Toolchain team
authenticationProvider: OAuth2
environment: development

extensions:
guacamole:
baseURI: http://localhost:8080/guacamole
Expand Down
Loading

0 comments on commit 0b0eb42

Please sign in to comment.