Skip to content

Commit

Permalink
Initial functionality (#1)
Browse files Browse the repository at this point in the history
* Add basic provider functionality

* Add basic group functionality

* Restructure code

* Use iterator instead of iterable in get_members

* WIP: Persist group information in database

* WIP: Use indico.core.db.db

* Restructure code

* Disable indico pytest plugins

* Create Indico plugin

* Add integration tests

* Add/fix tests

* Adapt code so that tox run passes

* Remove memory group provider

* Use random values in tests

* Add setup configuration

* Set ignore_missing_imports to true for mypy

* Add README

* Add Github Actions

* Add note to README that SAML auth provider must be used

* Shorten requirements.txt

* ci: Install requirements for xmlsec

* Init app after provider registration in tests

* Add experimental status warning to README

* Add license header to migration module

* Add pre-run-script to install xmlsec deps

* Add libpq-dev to install-packages.sh

* Adapt README

* Change author to is-devops in setup.cfg

* Add blank line to EOF

* Change author to myself

* Remove sudo from install-libs.sh

* Explain why pylint:no-member has been disabled

* Revert "Change author to myself"

This reverts commit eca8641.

* Add docstrings to methods in sql module
  • Loading branch information
cbartz authored Jul 31, 2023
1 parent b5cad84 commit 8941740
Show file tree
Hide file tree
Showing 34 changed files with 2,166 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/test-pre-script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

# Call script with sudo to install the required native libraries for installing the plugin's dependencies
sudo bash -xe "$(dirname "$0")"/../install-libs.sh
18 changes: 18 additions & 0 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Integration tests

on:
pull_request:

jobs:
integration-tests:
runs-on: ubuntu-latest
name: Integration Tests
steps:
- uses: actions/checkout@v3
- name: Install required native libraries
run: sudo bash -xe install-libs.sh
- name: Install tox
run: python3 -m pip install tox
- name: Run integration tests
run: |
tox -e integration
11 changes: 11 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Tests

on:
pull_request:

jobs:
unit-tests:
uses: canonical/operator-workflows/.github/workflows/test.yaml@main
secrets: inherit
with:
pre-run-script: .github/test-pre-script.sh
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Flask-Multipass-SAML-Groups

This package provides an identity provider for [Flask-Multipass](https://github.com/indico/flask-multipass),
which allows you to use SAML groups. It is designed to be used
as a plugin for [Indico](https://github.com/indico/indico).

> **Warning**
> The current code base has not been extensively tested and should be considered experimental.

## Motivation

The current SAML identity provider in Flask-Multipass does not support groups (see [issue](https://github.com/indico/flask-multipass/issues/66)),
but groups are a very useful feature for Indico. This plugin provides a solution to this problem.


## Installation

### Package installation
You need to install the package on the same virtual environment as your Indico instance.
You might use the following commands to switch to the Indico environment

```bash
su - indico
source ~/.venv/bin/activate
```

Some of the dependencies, like [xmlsec](https://xmlsec.readthedocs.io/en/stable/install.html),
require native libraries to be installed on the system. To install these libraries on an
Ubuntu system, you can use the `install-packages.sh` file:

```bash
sudo bash install-libs.sh
```

You can then install this package either via local source:

```bash
git clone https://github.com/canonical/flask-multipass-saml-groups.git
cd flask-multipass-saml-groups
python setup.py install
```

or with pip:

```bash
pip install git+https://github.com/canonical/flask-multipass-saml-groups.git
```


### Indico setup

In your Indico setup, you should see that the plugin is now available:

```bash
indico setup list-plugins
```

In order to activate the plugin, you must add it to the list of active plugins in your Indico configuration file:

```python
PLUGINS = { ..., 'saml_groups' }
```

Beyond that, the plugin uses its own database tables to persist the groups. Therefore you need to run

```bash
indico db --all-plugins upgrade
```
See [here](https://docs.getindico.io/en/latest/installation/plugins/) for more information on installing
Indico plugins.


### Identity provider configuration
The configuration is almost identical to the SAML identity provider in Flask-Multipass,
but you should use the type `saml_groups` instead of `saml`. The identity provider must be used
together with the SAML auth Provider, in order to receive the SAML groups in the authentication
data.

The following is an example section in `indico.conf`:
```python

_my_saml_config = {
'sp': {
'entityId': 'https://events.example.com',
'x509cert': '',
'privateKey': '',
},
'idp': {
'entityId': 'https://login.example.com',
'x509cert': 'YmFzZTY0IGVuY29kZWQgY2VydAo',
'singleSignOnService': {
'url': 'https://login.example.com/saml/',
'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
},
'singleLogoutService': {
'url': 'https://login.example.com/+logout',
'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
}
},
'security': {
'nameIdEncrypted': False,
'authnRequestsSigned': False,
'logoutRequestSigned': False,
'logoutResponseSigned': False,
'signMetadata': False,
'wantMessagesSigned': False,
'wantAssertionsSigned': False,
'wantNameId' : False,
'wantNameIdEncrypted': False,
'wantAssertionsEncrypted': False,
'allowSingleLabelDomains': False,
'signatureAlgorithm': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'digestAlgorithm': 'http://www.w3.org/2001/04/xmlenc#sha256'
},
}

MULTIPASS_AUTH_PROVIDERS = {
'ubuntu': {
'type': 'saml',
'title': 'SAML SSO',
'saml_config': _my_saml_config,
},
}
IDENTITY_PROVIDERS = {
"ubuntu": {
"type": "saml_groups",
"trusted_email": True,
"mapping": {
"user_name": "username",
"first_name": "fullname",
"last_name": "",
"email": "email",
},
"identifier_field": "openid",
}
}
```
4 changes: 4 additions & 0 deletions flask_multipass_saml_groups/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""The package containing the SAML Groups plugin."""
4 changes: 4 additions & 0 deletions flask_multipass_saml_groups/group_provider/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""The package containing the group providers for the SAML Groups plugin."""
85 changes: 85 additions & 0 deletions flask_multipass_saml_groups/group_provider/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
"""Defines the interface for a group provider."""

from abc import ABCMeta, abstractmethod
from typing import Iterable, Optional

from flask_multipass import Group, IdentityProvider


class GroupProvider(metaclass=ABCMeta):
"""A group provider is responsible for managing groups and their members.
Attrs:
group_class (type): The class to use for groups.
"""

group_class = Group

def __init__(self, identity_provider: IdentityProvider):
"""Initialize the group provider.
Args:
identity_provider: The associated identity provider. Usually required because the group
needs to know the identity provider.
"""

@abstractmethod
def add_group(self, name: str) -> None: # pragma: no cover
"""Add a group.
Args:
name: The name of the group.
"""

@abstractmethod
def get_group(self, name: str) -> Optional[Group]: # pragma: no cover
"""Get a group.
Args:
name: The name of the group.
Returns:
The group or None if it does not exist.
"""
return None

@abstractmethod
def get_groups(self) -> Iterable[Group]: # pragma: no cover
"""Get all groups.
Returns:
An iterable of all groups.
"""
return []

@abstractmethod
def get_user_groups(self, identifier: str) -> Iterable[Group]: # pragma: no cover
"""Get all groups a user is a member of.
Args:
identifier: The unique user identifier used by the provider.
Returns:
iterable: An iterable of groups the user is a member of.
"""
return []

@abstractmethod
def add_group_member(self, identifier: str, group_name: str) -> None: # pragma: no cover
"""Add a user to a group.
Args:
identifier: The unique user identifier used by the provider.
group_name: The name of the group.
"""

@abstractmethod
def remove_group_member(self, identifier: str, group_name: str) -> None: # pragma: no cover
"""Remove a user from a group.
Args:
identifier: The unique user identifier used by the provider.
group_name: The name of the group.
"""
Loading

0 comments on commit 8941740

Please sign in to comment.