Skip to content

Commit

Permalink
Merge branch 'main' into mpj/split-test-api-users
Browse files Browse the repository at this point in the history
  • Loading branch information
mjeammet authored Dec 17, 2024
2 parents f0d2a85 + bcdc579 commit f0e63bf
Show file tree
Hide file tree
Showing 31 changed files with 1,027 additions and 76 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ and this project adheres to

## [Unreleased]

### Fixed

- 🐛(backend) fix manage roles on domain admin view

### Added

- ✨(organizations) add siret to name conversion #584
- 💄(frontend) redirect home according to abilities #588
- ✨(maildomain_access) add API endpoint to search users #508

## [1.8.0] - 2024-12-12

### Added
Expand Down
Empty file modified bin/start-kind.sh
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ services:

dimail:
entrypoint: /opt/dimail-api/start-dev.sh
image: registry.mim-libre.fr/dimail/dimail-api:v0.0.16
image: registry.mim-libre.fr/dimail/dimail-api:v0.0.20
pull_policy: always
environment:
DIMAIL_MODE: FAKE
Expand Down
115 changes: 115 additions & 0 deletions docs/local_development_kube.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Local development with Kubernetes

We use tilt to provide a local development environment for Kubernetes.
Tilt is a tool that helps you develop applications for Kubernetes.
It watches your files for changes, rebuilds your containers, and restarts your pods.
It's like having a conversation with your cluster.


## Prerequisites

This guide assumes you have the following tools installed:

- [Docker](https://docs.docker.com/get-docker/)
- [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/)
* [mkcert](https://github.com/FiloSottile/mkcert)
- [ctlptl](https://github.com/tilt-dev/ctlptl)
- [Tilt](https://docs.tilt.dev/install.html)
* [helm](https://helm.sh/docs/intro/install/)
* [helmfile](https://github.com/helmfile/helmfile)
* [secrets](https://github.com/jkroepke/helm-secrets/wiki/Installation)
* [sops](https://github.com/getsops/sops)


### SOPS configuration

**Generate a SOPS key**

For this specific step you need to have the `age-keygen` tool installed.
See https://github.com/FiloSottile/age.
Then generate a key:

```bash
age-keygen -o my-age.key
```

**Install the SOPS key**

Read the SOPS documentation on how to install the key in your environment.
https://github.com/getsops/sops?tab=readme-ov-file#22encrypting-using-age

On Ubuntu it's like:

```bash
mkdir -p ~/.config/sops/age/
cp my-age.key ~/.config/sops/age/keys.txt
chmod 400 ~/.config/sops/age/keys.txt
```

**Add the SOPS key to the repository**

Update the [.sops.yaml](../.sops.yaml) file with the **public** key id you generated.


### Helmfile in Docker

If you use helmfile in Docker, you may need an additional configuration to make
it work with you age key.

You need to mount `-v "${HOME}/.config/sops/age/:/helm/.config/sops/age/"`

```bash
#!/bin/sh

docker run --rm --net=host \
-v "${HOME}/.kube:/root/.kube" \
-v "${HOME}/.config/helm:/root/.config/helm" \
-v "${HOME}/.config/sops/age/:/helm/.config/sops/age/" \
-v "${HOME}/.minikube:/${HOME}/.minikube" \
-v "${PWD}:/wd" \
-e KUBECONFIG=/root/.kube/config \
--workdir /wd ghcr.io/helmfile/helmfile:v0.150.0 helmfile "$@"
```


## Getting started

### Create the kubernetes cluster

Run the following command to create a kubernetes cluster using kind:

```bash
./bin/start-kind.sh
```

**or** run the equivalent using the makefile

```bash
make start-kind
```

### Deploy the application

```bash
tilt up -f ./bin/Tiltfile
```

**or** run the equivalent using the makefile

```bash
make tilt-up
```

That's it! You should now have a local development environment for Kubernetes.

You can access the application at https://desk.127.0.0.1.nip.io

## Management

To manage the cluster, you can use k9s.

## Next steps

- Add dimail to the local development environment
- Add a reset demo `cmd_button` to Tilt
11 changes: 11 additions & 0 deletions src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from timezone_field import TimeZoneField

from core.enums import WebhookStatusChoices
from core.plugins.loader import organization_plugins_run_after_create
from core.utils.webhooks import scim_synchronizer
from core.validators import get_field_validators_from_setting

Expand Down Expand Up @@ -286,6 +287,16 @@ def get_or_create_from_user_claims(

raise ValueError("Should never reach this point.")

def create(self, **kwargs):
"""
Create an organization with the given kwargs.
This method is overridden to call the Organization plugins.
"""
instance = super().create(**kwargs)
organization_plugins_run_after_create(instance)
return instance


class Organization(BaseModel):
"""
Expand Down
1 change: 1 addition & 0 deletions src/backend/core/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Core plugins package."""
13 changes: 13 additions & 0 deletions src/backend/core/plugins/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Base plugin class for organization plugins."""


class BaseOrganizationPlugin:
"""
Base class for organization plugins.
Plugins must implement all methods of this class even if it is only to "pass".
"""

def run_after_create(self, organization) -> None:
"""Method called after creating an organization."""
raise NotImplementedError("Plugins must implement the run_after_create method")
32 changes: 32 additions & 0 deletions src/backend/core/plugins/loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Helper functions to load and run organization plugins."""

from functools import lru_cache
from typing import List

from django.conf import settings
from django.utils.module_loading import import_string

from core.plugins.base import BaseOrganizationPlugin


@lru_cache(maxsize=None)
def get_organization_plugins() -> List[BaseOrganizationPlugin]:
"""
Return a list of all organization plugins.
While the plugins initialization does not depend on the request, we can cache the result.
"""
return [
import_string(plugin_path)() for plugin_path in settings.ORGANIZATION_PLUGINS
]


def organization_plugins_run_after_create(organization):
"""
Run the after create method for all organization plugins.
Each plugin will be called in the order they are listed in the settings.
Each plugin is responsible to save changes if needed, this is not optimized
but this could be easily improved later if needed.
"""
for plugin_instance in get_organization_plugins():
plugin_instance.run_after_create(organization)
18 changes: 14 additions & 4 deletions src/backend/mailbox_manager/api/client/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Client serializers for People's mailbox manager app."""

import json
from logging import getLogger

from requests.exceptions import HTTPError
from rest_framework import exceptions, serializers

from core.api.client.serializers import UserSerializer
Expand All @@ -10,6 +12,8 @@
from mailbox_manager import enums, models
from mailbox_manager.utils.dimail import DimailAPIClient

logger = getLogger(__name__)


class MailboxSerializer(serializers.ModelSerializer):
"""Serialize mailbox."""
Expand Down Expand Up @@ -190,14 +194,20 @@ def create(self, validated_data):
"""
dimail = DimailAPIClient()

user = validated_data["user"]
domain = validated_data["domain"]

if validated_data["role"] in [
enums.MailDomainRoleChoices.ADMIN,
enums.MailDomainRoleChoices.OWNER,
]:
dimail.create_user(validated_data["user"].sub)
dimail.create_allow(
validated_data["user"].sub, validated_data["domain"].name
)
try:
dimail.create_user(user.sub)
dimail.create_allow(user.sub, domain.name)
except HTTPError:
logger.exception("[DIMAIL] access creation failed %s")
domain.status = enums.MailDomainStatusChoices.FAILED
domain.save()

return super().create(validated_data)

Expand Down
30 changes: 29 additions & 1 deletion src/backend/mailbox_manager/api/client/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""API endpoints"""

from django.db.models import Subquery
from django.db.models import Q, Subquery

from rest_framework import exceptions, filters, mixins, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from core import models as core_models
from core.api.client.serializers import UserSerializer

from mailbox_manager import enums, models
from mailbox_manager.api import permissions
Expand Down Expand Up @@ -76,6 +77,9 @@ class MailDomainAccessViewSet(
Return list of all domain accesses related to the logged-in user and one
domain access if an id is provided.
GET /api/v1.0/mail-domains/<domain_slug>/accesses/users/
Return list of all users who can have an access to the domain
POST /api/v1.0/mail-domains/<domain_slug>/accesses/ with expected data:
- user: str
- role: str [owner|admin|viewer]
Expand Down Expand Up @@ -183,6 +187,30 @@ def destroy(self, request, *args, **kwargs):

return super().destroy(request, *args, **kwargs)

@action(detail=False, url_path="users", methods=["get"])
def get_available_users(self, request, domain_slug):
"""API endpoint to search user to give them new access.
More filters and permission will be added soon.
"""
domain = models.MailDomain.objects.get(slug=domain_slug)
abilities = domain.get_abilities(request.user)
if not abilities["manage_accesses"]:
raise exceptions.PermissionDenied()

queryset = (
core_models.User.objects.order_by("-created_at")
# exclude inactive users and get users from identified user's organization
.filter(is_active=True, organization_id=request.user.organization_id)
# exclude all users with already an access config
.exclude(mail_domain_accesses__domain__slug=domain_slug)
)
# Search by case-insensitive and accent-insensitive
if query := request.GET.get("q", ""):
queryset = queryset.filter(
Q(name__unaccent__icontains=query) | Q(email__unaccent__icontains=query)
)
return Response(UserSerializer(queryset.all(), many=True).data)


class MailBoxViewSet(
mixins.CreateModelMixin,
Expand Down
Loading

0 comments on commit f0e63bf

Please sign in to comment.