Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
depends_on:
- postgres
- redis
image: certpl/mwdb:v2.17.0
image: certpl/mwdb:v2.18.0
restart: on-failure
env_file:
# NOTE: use gen_vars.sh in order to generate this file
Expand All @@ -22,7 +22,7 @@ services:
build:
context: .
dockerfile: deploy/docker/Dockerfile-web
image: certpl/mwdb-web:v2.17.0
image: certpl/mwdb-web:v2.18.0
ports:
- "80:80"
restart: on-failure
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
author = 'CERT Polska'

# The full version, including alpha/beta/rc tags
release = '2.17.0'
release = '2.18.0'


# -- General configuration ---------------------------------------------------
Expand Down
35 changes: 35 additions & 0 deletions docs/oauth-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,41 @@ login turned on, but don't pass set password link to users - you can achieve thi
and removing credentials part. To set up your own templates, change ``mail_templates_dir`` in configuration to point at your folder,
copy templates from https://github.com/CERT-Polska/mwdb-core/tree/master/mwdb/templates/mail and modify them accordingly.

Manage MWDB Groups from OpenID Groups
-------------------------------------

If your identity provider is correctly configured to return the user groups in the OpenID Token,
you can map automatically these groups to MWDB groups.

These feature can be enable or disable per provider directly in the administration interface.

Three modes are available:
- NONE: The feature is disable. The groups included in the OIDC Token are ignored
- FULL: The user groups are fully managed by the list of groups included in their OIDC Token.
Users will be added to all the MWDB groups included in their OIDC Token and removed from the others whatever the groups providers of origin.
It means that a user can be automatically removed from local MWDB groups if these groups are not listed in their OIDC token
- MIXED: The OIDC users are only added or removed from the MWDB groups of the current OIDC provider.
The local groups or groups from other providers remain unchanged whatever the groups listed in their OIDC token.

You can filter the groups coming from OIDC using the parameter: ``OIDC groups matching pattern=(.*)``
This variable shall contain a regular expression. It is used to filter and map the external to internal group names.
Only the external group names matching the regular expression are managed, the other ones are ignored.

The mapping between external and internal group names is performed using the parameter: ``OIDC groups replacing pattern=\1``
It uses the result of the previous regular expression to build the internal names.
Regular expression groups are supported.

.. note::

You can for example match only the groups starting by MWDB_xxx by configuration the parameter OIDC groups matching pattern to ``MWDB_(.*)``

And map these groups to internal groups as EXTERNAL_xxx by configuration the parameter OIDC groups replacing pattern to ``EXTERNAL_\1``

With this configuration, the group name MWDB_MALWARE_ANALYSTS will be mapped internally to the local group EXTERNAL_MALWARE_ANALYST


By default this feature is disable

Disable password-based authentication
-------------------------------------

Expand Down
19 changes: 10 additions & 9 deletions mwdb/core/oauth/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from marshmallow import ValidationError
from sqlalchemy import exists

from mwdb.schema.group import GroupNameSchemaBase
from mwdb.schema.user import UserLoginSchemaBase

from .client import OpenIDClient
Expand Down Expand Up @@ -53,15 +54,6 @@ def get_group_name(self) -> str:
"""
return ("OpenID_" + self.name)[:32]

def create_provider_group(self) -> "Group":
"""
Creates a Group model object for a new OpenID provider
"""
from mwdb.model import Group

group_name = self.get_group_name()
return Group(name=group_name, immutable=True, workspace=False)

def iter_user_name_variants(self, sub: bytes, userinfo: UserInfo) -> Iterator[str]:
"""
Yield username variants that are used when user registers using OpenID identity
Expand All @@ -88,6 +80,15 @@ def get_user_email(self, sub: bytes, userinfo: UserInfo) -> str:
else:
return f"{sub}@mwdb.local"

def get_user_groups(self, claims: dict[str, object]) -> list[str]:
"""
User groups that are used when user registers using OpenID identity
"""
if "groups" in claims.keys():
return claims["groups"]
else:
return []

def get_user_description(self, sub: bytes, userinfo: UserInfo) -> str:
"""
User description that is used when user registers using OpenID identity
Expand Down
9 changes: 8 additions & 1 deletion mwdb/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ def after_cursor_execute(conn, cursor, statement, parameters, context, executema
from .file import File # noqa: E402
from .group import Group, Member # noqa: E402
from .karton import KartonAnalysis, karton_object # noqa: E402
from .oauth import OpenIDProviderSettings, OpenIDUserIdentity # noqa: E402
from .oauth import ( # noqa: E402
OpenIDGroupManagementMode,
OpenIDProviderSettings,
OpenIDUserIdentity,
)
from .object import Object, relation # noqa: E402
from .object_permission import ObjectPermission # noqa: E402
from .quick_query import QuickQuery # noqa: E402
Expand All @@ -75,6 +79,9 @@ def after_cursor_execute(conn, cursor, statement, parameters, context, executema
"Object",
"ObjectPermission",
"OpenIDProviderSettings",
"OpenIDGroupManagementMode",
"DEFAULT_OIDC_GROUPS_MATCH_PATTERN",
"DEFAULT_OIDC_GROUPS_REPLACE_PATTERN",
"OpenIDUserIdentity",
"relation",
"QuickQuery",
Expand Down
19 changes: 19 additions & 0 deletions mwdb/model/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ class Group(db.Model):
workspace = db.Column(db.Boolean, nullable=False, default=True)
immutable = db.Column(db.Boolean, nullable=False, default=False)

# External group created by OpenID Provider
openid_provider_id = db.Column(
db.Integer,
db.ForeignKey("openid_provider.id"),
nullable=True,
default=None,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better if FK is set to openid_provider.id because id is the PK (even if name has unique constraint). I see why name was chosen - only name is passed to mwdb.core.oauth.provider.OpenIDProvider object and create_user_groups requires reference to the provider entity. I think it would be better to change the interface of OpenIDProvider object interface instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


openid_provider = db.relationship(
"OpenIDProviderSettings",
back_populates="openid_groups",
foreign_keys=[openid_provider_id],
lazy="select",
)

members = db.relationship(
"Member", back_populates="group", cascade="all, delete-orphan"
)
Expand Down Expand Up @@ -66,6 +81,10 @@ def user_logins(self):
def group_admins(self):
return [member.user.login for member in self.members if member.group_admin]

@property
def provider_name(self):
return self.openid_provider.name

def add_member(self, user):
if user in self.users:
return False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Add group oidc provider

Revision ID: 7nsxysyisgrc
Revises: 01dea56ffaf7
Create Date: 2026-02-24 10:36:52.350767

"""
import sqlalchemy as sa
from alembic import op

from mwdb.model import OpenIDGroupManagementMode

# revision identifiers, used by Alembic.
revision = "7nsxysyisgrc"
down_revision = "01dea56ffaf7"
branch_labels = None
depends_on = None

def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("group", sa.Column("openid_provider_id", sa.Integer, nullable=True))
op.create_foreign_key(
None, "group", "openid_provider", ["openid_provider_id"], ["id"], ondelete="SET NULL"
)

op.execute(
"CREATE TYPE openidgroupmanagementmode AS ENUM ('NONE', 'FULL', 'MIXED');"
)

op.add_column("openid_provider", sa.Column("oidc_groups_management_mode", sa.Enum(OpenIDGroupManagementMode), nullable=False))
op.add_column("openid_provider", sa.Column("oidc_groups_match_pattern", sa.Text(), nullable=False))
op.add_column("openid_provider", sa.Column("oidc_groups_replace_pattern", sa.Text(), nullable=False))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "group", type_="foreignkey")
op.drop_column("group", "openid_provider_id")

op.drop_column("openid_provider", "oidc_groups_management_mode")
op.drop_column("openid_provider", "oidc_groups_match_pattern")
op.drop_column("openid_provider", "oidc_groups_replace_pattern")
# ### end Alembic commands ###
22 changes: 22 additions & 0 deletions mwdb/model/oauth.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from enum import Enum
from typing import Type

from mwdb.core.oauth.provider import OpenIDProvider

from . import db
from .group import Group


def get_oidc_provider_class(provider_name: str) -> Type[OpenIDProvider]:
Expand All @@ -11,6 +13,12 @@ def get_oidc_provider_class(provider_name: str) -> Type[OpenIDProvider]:
return openid_provider_classes.get(provider_name, OpenIDProvider)


class OpenIDGroupManagementMode(Enum):
NONE = "NONE"
FULL = "FULL"
MIXED = "MIXED"


class OpenIDProviderSettings(db.Model):
__tablename__ = "openid_provider"

Expand All @@ -24,6 +32,11 @@ class OpenIDProviderSettings(db.Model):
jwks_endpoint = db.Column(db.Text, nullable=True)
logout_endpoint = db.Column(db.Text, nullable=True)
requires_approval = db.Column(db.Boolean, nullable=False, default=False)
oidc_groups_management_mode = db.Column(
db.Enum(OpenIDGroupManagementMode), nullable=False
)
oidc_groups_match_pattern = db.Column(db.Text, nullable=False)
oidc_groups_replace_pattern = db.Column(db.Text, nullable=False)

group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)

Expand All @@ -32,8 +45,17 @@ class OpenIDProviderSettings(db.Model):
back_populates="provider",
cascade="all, delete-orphan",
)

openid_groups = db.relationship(
"Group",
foreign_keys=[Group.openid_provider_id],
back_populates="openid_provider",
lazy="select",
)

group = db.relationship(
"Group",
foreign_keys=[group_id],
cascade="all, delete",
)

Expand Down
10 changes: 8 additions & 2 deletions mwdb/resources/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ def get(self):
"""
objs = (
db.session.query(Group).options(
joinedload(Group.members), joinedload(Group.members, Member.user)
joinedload(Group.members),
joinedload(Group.members, Member.user),
joinedload(Group.openid_provider),
)
).all()
schema = GroupListResponseSchema()
Expand Down Expand Up @@ -100,7 +102,11 @@ def get(self, name):
"""
obj = (
db.session.query(Group)
.options(joinedload(Group.members), joinedload(Group.members, Member.user))
.options(
joinedload(Group.members),
joinedload(Group.members, Member.user),
joinedload(Group.openid_provider),
)
.filter(Group.name == name)
).first()
if obj is None:
Expand Down
Loading
Loading