Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an add-admin action #220

Merged
merged 32 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2a864cc
Add an add-user action
nrobinaubertin Jan 22, 2023
4f81db7
Add unit tests for add-user action
nrobinaubertin Jan 25, 2023
0fc1ebb
Change the CLI patch into a plugin
nrobinaubertin Jan 25, 2023
bb43d21
Fix unit tests
nrobinaubertin Jan 25, 2023
0098aca
Remove unused file
nrobinaubertin Jan 25, 2023
de9945f
Split unit tests in two files
nrobinaubertin Jan 26, 2023
77dc42e
Use tpying.List instead of list
nrobinaubertin Jan 26, 2023
5f087f2
Remove unused files
nrobinaubertin Jan 26, 2023
20992f7
Fix docstring
nrobinaubertin Jan 26, 2023
a05206e
Add 'expected_' prefix to a variable's name
nrobinaubertin Jan 26, 2023
ce5bbe6
Add stdout message when something goes wrong
nrobinaubertin Jan 26, 2023
9f3c9c4
Remove unused import
nrobinaubertin Jan 26, 2023
f08fb27
Remove ignores
nrobinaubertin Jan 27, 2023
c9c9e4f
Change action into add-admin (only adds admins)
nrobinaubertin Jan 30, 2023
c6cc492
Add integration test for add-admin
nrobinaubertin Jan 30, 2023
083e21f
Fix author and email for autocreate plugin
nrobinaubertin Jan 30, 2023
0acccc2
Add missing comments and copyright notices
nrobinaubertin Jan 31, 2023
4128088
Fix docstring of the added integ test
nrobinaubertin Jan 31, 2023
bc35a06
Add comment explaining #nosec
nrobinaubertin Jan 31, 2023
af94b49
Add plugins files to the linting tests
nrobinaubertin Jan 31, 2023
8c905aa
Add missing copyright
nrobinaubertin Jan 31, 2023
c348acb
Log exception's stdout
nrobinaubertin Jan 31, 2023
42b8374
Check that the created user is admin before returning it
nrobinaubertin Feb 2, 2023
99b04a9
Change var names in dict comprehension
nrobinaubertin Feb 2, 2023
06140cb
Fix unit test
nrobinaubertin Feb 2, 2023
b3713e7
Fix typo
nrobinaubertin Feb 2, 2023
6337e55
Merge origin/main
nrobinaubertin Feb 2, 2023
03c46a3
Remove pylint disable by renaming var
nrobinaubertin Feb 3, 2023
f0ee5f2
Add a .mypy_cache to be ignored
nrobinaubertin Feb 3, 2023
e505648
Merge remote-tracking branch 'origin/main' into add-user
nrobinaubertin Feb 3, 2023
ec13f9e
Add a tox env and github ci job for the plugins
nrobinaubertin Feb 3, 2023
d7db4c4
Merge remote-tracking branch 'origin/main' into add-user
nrobinaubertin Feb 3, 2023
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

# Include the files directory
!files/*
!plugins/*
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ __pycache__/
.idea
.vscode
.mypy_cache
*.egg-info/
10 changes: 10 additions & 0 deletions actions.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
refresh-external-resources:
description: Pull changes from the customization repository, reload uWSGI and upgrade the external plugins
add-admin:
description: Add an admin to Indico
params:
email:
type: string
description: User email.
password:
type: string
description: User password.
required: [email, password]
6 changes: 5 additions & 1 deletion indico.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ RUN apt-get update \
&& addgroup --gid ${indico_gid} indico \
&& adduser --system --gid ${indico_gid} --uid ${indico_uid} --home /srv/indico indico

# Add our plugins
COPY --chown=indico:indico plugins /srv/indico/plugins

USER indico
RUN python3 -m pip install --no-cache-dir --no-warn-script-location --prefer-binary \
indico==3.2.0 \
indico-plugin-piwik \
indico-plugin-storage-s3 \
python-ldap \
python3-saml \
python-ldap \
/srv/indico/plugins/autocreate \
uwsgi \
&& /bin/bash -c "mkdir -p --mode=775 /srv/indico/{archive,cache,custom,etc,log,tmp}" \
&& /bin/bash -c "chown indico:indico /srv/indico /srv/indico/{archive,cache,custom,etc,log,tmp,.local}" \
Expand Down
3 changes: 3 additions & 0 deletions plugins/autocreate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Autocreate Plugin

Extends the indico CLI to add a non-interactive way to create users
5 changes: 5 additions & 0 deletions plugins/autocreate/autocreate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""Create users non-interactively."""
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved
74 changes: 74 additions & 0 deletions plugins/autocreate/autocreate/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""Create users non-interactively."""

import click
from indico.cli.core import cli_group
from indico.core.db import db
from indico.modules.auth import Identity
from indico.modules.users import User
from indico.modules.users.operations import create_user
from indico.modules.users.util import search_users


@cli_group(name="autocreate")
def cli():
"""Create users non-interactively."""


@cli.command("admin")
@click.argument("email", type=str)
@click.argument("password", type=str)
@click.pass_context
def create_admin(ctx, email, password):
"""Create a new admin user non-interactively."""

email = email.lower()
username = email
first_name = "unknown"
last_name = "unknown"
affiliation = "unknown"

if User.query.filter(User.all_emails == email, ~User.is_deleted, ~User.is_pending).has_rows():
click.secho("This user already exists", fg="red")
ctx.exit(1)

if password == "":
click.secho("Password should not be empty", fg="red")
ctx.exit(1)

if Identity.query.filter_by(provider="indico", identifier=username).has_rows():
click.secho("Username already exists", fg="red")
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved
ctx.exit(1)

identity = Identity(provider="indico", identifier=username, password=password)
user = create_user(
email,
{"first_name": first_name, "last_name": last_name, "affiliation": affiliation},
identity,
)
user.is_admin = True

# db.session has add()
db.session.add(user) # pylint: disable=no-member
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved
# db.session has commit()
db.session.commit() # pylint: disable=no-member

# search the created user
res = search_users(
exact=True,
include_deleted=False,
include_pending=False,
include_blocked=False,
external=False,
allow_system_user=False,
email=email,
)

if not res:
click.secho("Admin was not correctly created", fg="red")
ctx.exit(1)

click.secho(f'Admin with email "{res.pop().email}" correctly created', fg="green")
23 changes: 23 additions & 0 deletions plugins/autocreate/autocreate/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""Connect our CLI plugin to the existing Indico's CLI."""

from autocreate.cli import cli
from indico.core import signals
from indico.core.plugins import IndicoPlugin


class AutocreatePlugin(IndicoPlugin):
"""Autocreate

Provides a way to non-interactively create users via Indico's CLI
"""

def init(self):
super().init()
self.connect(signals.plugin.cli, self._extend_indico_cli)

def _extend_indico_cli(self):
return cli
28 changes: 28 additions & 0 deletions plugins/autocreate/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[metadata]
name = indico-plugin-autocreate
version = 3.2
description = Extends the indico CLI to add a non-interactive way to create users
long_description = file: README.md
long_description_content_type = text/markdown; charset=UTF-8; variant=GFM
url = https://github.com/canonical/indico-operator
license = Apache License 2.0
author = launchpad.net/~canonical-is-devops
author_email = [email protected]
classifiers =
Environment :: Plugins
Environment :: Web Environment
License :: OSI Approved :: Apache Software License
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved

[options]
packages = find:
zip_safe = false
include_package_data = true
python_requires = >=3.9.0, <3.11
install_requires =
indico>=3.2

[options.entry_points]
indico.plugins =
autocreate = autocreate.plugin:AutocreatePlugin
5 changes: 5 additions & 0 deletions plugins/autocreate/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Extends the indico CLI to add a non-interactive way to create users."""

from setuptools import setup

setup()
42 changes: 42 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
)
from ops.pebble import ExecError

logger = logging.getLogger(__name__)

CANONICAL_LDAP_HOST = "ldap.canonical.com"
CELERY_PROMEXP_PORT = "9808"
DATABASE_NAME = "indico"
Expand Down Expand Up @@ -70,6 +72,7 @@ def __init__(self, *args):
self.on.refresh_external_resources_action, self._refresh_external_resources_action
)
# self.framework.observe(self.on.update_status, self._refresh_external_resources)
self.framework.observe(self.on.add_admin_action, self._add_admin_action)

self._stored.set_default(
db_conn_str=None,
Expand Down Expand Up @@ -689,6 +692,10 @@ def _get_indico_env_config(self, container: Container) -> Dict:
env_config = {**env_config, **self._get_http_proxy_configuration()}
return env_config

def _get_indico_env_config_str(self, container: Container) -> Dict[str, str]:
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved
indico_env_config = self._get_indico_env_config(container)
return {k: str(v) for k, v in indico_env_config.items()}
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved

def _get_http_proxy_configuration(self) -> Dict[str, str]:
"""Generate http proxy config.

Expand Down Expand Up @@ -905,6 +912,41 @@ def _has_secrets(self) -> bool:
# check if the other end of a relation also supports secrets...
return juju_version.has_secrets

def _add_admin_action(self, event: ActionEvent) -> None:
"""Add a new user to Indico.

Args:
event: Event triggered by the add_admin action
"""
container = self.unit.get_container("indico")
indico_env_config = self._get_indico_env_config_str(container)

cmd = [
"/srv/indico/.local/bin/indico",
"autocreate",
"admin",
event.params["email"],
event.params["password"],
]

if container.can_connect():
process = container.exec(
cmd,
user="indico",
working_dir="/srv/indico",
environment=indico_env_config,
)
try:
output = process.wait_output()
event.set_results({"user": f"{event.params['email']}", "output": output})
except ExecError as ex:
logger.exception("Action add-admin failed: %s", ex.stdout)

event.fail(
# Parameter validation errors are printed to stdout
f"Failed to create admin {event.params['email']}: {ex.stdout!r}"
)


if __name__ == "__main__": # pragma: no cover
main(IndicoOperatorCharm, use_juju_for_storage=True)
29 changes: 28 additions & 1 deletion tests/integration/test_charm.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# Copyright 2022 Canonical Ltd.
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

"""Indico charm integration tests."""

import juju.action
import pytest
import requests
from ops.model import ActiveStatus, Application
Expand Down Expand Up @@ -94,3 +95,29 @@ async def test_health_checks(app: Application):
assert stdout.count("0/3") == 1
else:
assert stdout.count("0/3") == 2


@pytest.mark.asyncio
@pytest.mark.abort_on_fail
async def test_add_admin(app: Application):
"""
arrange: given charm in its initial state
act: run the add-admin action
assert: check the output in the action result
"""

# Application actually does have units
assert app.units[0] # type: ignore
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved

email = "[email protected]"
# This is a test password
password = "somepassword" # nosec

# Application actually does have units
action: juju.action.Action = await app.units[0].run_action( # type: ignore
"add-admin", email=email, password=password
)
await action.wait()
assert action.status == "completed"
assert action.results["user"] == email
assert f'Admin with email "{email}" correctly created' in action.results["output"]
nrobinaubertin marked this conversation as resolved.
Show resolved Hide resolved
Loading