Skip to content

Commit

Permalink
feat: manual users provisioning (#40)
Browse files Browse the repository at this point in the history
* feat: disable users auto provisioning with migration

Signed-off-by: Michal Fiedorowicz <[email protected]>

* python fmt

Signed-off-by: Michal Fiedorowicz <[email protected]>

* rename user category to user type

Signed-off-by: Michal Fiedorowicz <[email protected]>

* feat: add setup form

Signed-off-by: Michal Fiedorowicz <[email protected]>

* test for plugin config helpers

Signed-off-by: Michal Fiedorowicz <[email protected]>

* feat: add setup view

Signed-off-by: Michal Fiedorowicz <[email protected]>

* disable setup form field if existing or predefined api key is not found

Signed-off-by: Michal Fiedorowicz <[email protected]>

* update README

Signed-off-by: Michal Fiedorowicz <[email protected]>

* add setup warning alert

Signed-off-by: Michal Fiedorowicz <[email protected]>

* update setup warning copy

Signed-off-by: Michal Fiedorowicz <[email protected]>

* typo

Signed-off-by: Michal Fiedorowicz <[email protected]>

* refine setup view copy

Signed-off-by: Michal Fiedorowicz <[email protected]>

---------

Signed-off-by: Michal Fiedorowicz <[email protected]>
  • Loading branch information
mfiedorowicz authored Oct 14, 2024
1 parent 2531c74 commit 962e2d5
Show file tree
Hide file tree
Showing 14 changed files with 631 additions and 70 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ Also in your `configuration.py` file, in order to customise the plugin settings,
```python
PLUGINS_CONFIG = {
"netbox_diode_plugin": {
# Auto-provision users for Diode plugin
"auto_provision_users": False,

# Diode gRPC target for communication with Diode server
"diode_target_override": "grpc://localhost:8080/diode",

Expand All @@ -64,6 +67,11 @@ PLUGINS_CONFIG = {
Note: Once you customise usernames with PLUGINS_CONFIG during first installation, you should not change or remove them
later on. Doing so will cause the plugin to stop working properly.

`auto_provision_users` is a boolean flag (default: `False`) that determines whether the plugin should automatically
create the users during
migration. If set to `False`, you will need to provision Diode users with their API keys manually via the plugin's setup
page in the NetBox UI.

Restart NetBox services to load the plugin:

```
Expand Down Expand Up @@ -93,7 +101,16 @@ export NETBOX_TO_DIODE_API_KEY=$(head -c20 </dev/urandom|xxd -p); env | grep NET
export DIODE_API_KEY=$(head -c20 </dev/urandom|xxd -p); env | grep DIODE_API_KEY
```

**Note:** store these API key strings in a safe place as they will be needed later to configure the Diode server
**Note:** store these API key strings in a safe place as they will be needed later to configure the Diode server.

If you don't set these environment variables, the plugin will generate random API keys for you either during the
migration process (with `auto_provision_users` set to `True`) or when you manually create the users in the plugin's
setup page in the NetBox UI.

It's important to note that the environment variables with API keys should be populated in the Diode server's
environment variables (
see [docs](https://github.com/netboxlabs/diode/tree/develop/diode-server#running-the-diode-server)) as well to ensure
proper communication between the Diode SDK, Diode server and the NetBox plugin.

Run migrations to create all necessary resources:

Expand Down
3 changes: 3 additions & 0 deletions docker/netbox/configuration/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

# PLUGINS_CONFIG = {
# "netbox_diode_plugin": {
# # Auto-provision users for Diode plugin
# "auto_provision_users": True,
#
# # Diode gRPC target for communication with Diode server
# "diode_target_override": "grpc://localhost:8080/diode",
#
Expand Down
2 changes: 1 addition & 1 deletion docker/netbox/plugins_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@

PLUGINS_CONFIG = {
"netbox_diode_plugin": {
"enable_ingestion_logs": True,
"auto_provision_users": True,
}
}
5 changes: 2 additions & 3 deletions netbox_diode_plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ class NetBoxDiodePluginConfig(PluginConfig):
base_url = "diode"
min_version = "3.7.2"
default_settings = {
# Auto-provision users for Diode plugin
"auto_provision_users": False,
# Default Diode gRPC target for communication with Diode server
"diode_target": "grpc://localhost:8080/diode",

# User allowed for Diode to NetBox communication
"diode_to_netbox_username": "diode-to-netbox",

# User allowed for NetBox to Diode communication
"netbox_to_diode_username": "netbox-to-diode",

# User allowed for data ingestion
"diode_username": "diode-ingestion",
}
Expand Down
57 changes: 56 additions & 1 deletion netbox_diode_plugin/forms.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# !/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
"""Diode NetBox Plugin - Forms."""
from django import forms
from django.core.validators import MinLengthValidator
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm
from netbox.plugins import get_plugin_config
from users.models import Token as NetBoxToken
from utilities.forms.rendering import FieldSet

from netbox_diode_plugin.models import Setting

__all__ = ("SettingsForm",)
__all__ = (
"SettingsForm",
"SetupForm",
)


class SettingsForm(NetBoxModelForm):
Expand Down Expand Up @@ -38,3 +45,51 @@ def __init__(self, *args, **kwargs):
self.fields["diode_target"].help_text = (
"This field is not allowed to be modified."
)


class SetupForm(forms.Form):
"""Setup form."""

def __init__(self, users, *args, **kwargs):
"""Initialize the form."""
super().__init__(*args, **kwargs)

for user_type, user_properties in users.items():
field_name = f"{user_type}_api_key"
username_or_type = user_properties.get("username") or user_type
label = f"{username_or_type}"

disabled = user_properties.get("predefined_api_key") is not None or user_properties.get("api_key") is not None
help_text = _(
f"Key must be at least 40 characters in length.<br />Map to environment variable "
f'{user_properties["api_key_env_var_name"]} in Diode service'
f'{" and Diode SDK" if user_type == "diode" else ""}'
)

initial_value = user_properties.get("api_key") or user_properties.get(
"predefined_api_key"
)

if (
user_properties.get("predefined_api_key") is None
and user_properties.get("api_key") is None
):
initial_value = NetBoxToken.generate_key()

self.fields[field_name] = forms.CharField(
required=True,
max_length=40,
validators=[MinLengthValidator(40)],
label=label,
disabled=disabled,
initial=initial_value,
help_text=help_text,
widget=forms.TextInput(
attrs={
"data-clipboard": "true",
"placeholder": _(
f"Enter a valid API key for {username_or_type} user"
),
}
),
)
18 changes: 13 additions & 5 deletions netbox_diode_plugin/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.conf import settings as netbox_settings
from django.contrib.contenttypes.management import create_contenttypes
from django.db import migrations, models
from netbox.plugins import get_plugin_config
from users.models import Token as NetBoxToken

from netbox_diode_plugin.plugin_config import get_diode_usernames
Expand All @@ -24,7 +25,7 @@ def _read_secret(secret_name, default=None):
return f.readline().strip()


def _create_user_with_token(apps, user_category, username, group):
def _create_user_with_token(apps, user_type, username, group):
User = apps.get_model(netbox_settings.AUTH_USER_MODEL)
"""Create a user with the given username and API key if it does not exist."""
try:
Expand All @@ -37,7 +38,7 @@ def _create_user_with_token(apps, user_category, username, group):
Token = apps.get_model("users", "Token")

if not Token.objects.filter(user=user).exists():
key = f"{user_category.upper()}_API_KEY"
key = f"{user_type.upper()}_API_KEY"
api_key = _read_secret(key.lower(), os.getenv(key))
if api_key is None:
api_key = NetBoxToken.generate_key()
Expand Down Expand Up @@ -66,11 +67,18 @@ def configure_plugin(apps, schema_editor):
)
permission.object_types.set([diode_plugin_object_type.id])

auto_provision_users = get_plugin_config(
"netbox_diode_plugin", "auto_provision_users"
)

if not auto_provision_users:
return

diode_to_netbox_user_id = None

for user_category, username in get_diode_usernames().items():
user = _create_user_with_token(apps, user_category, username, group)
if user_category == "diode_to_netbox":
for user_type, username in get_diode_usernames().items():
user = _create_user_with_token(apps, user_type, username, group)
if user_type == "diode_to_netbox":
diode_to_netbox_user_id = user.id

permission.users.set([diode_to_netbox_user_id])
Expand Down
8 changes: 4 additions & 4 deletions netbox_diode_plugin/migrations/0004_rename_legacy_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@

def rename_legacy_users(apps, schema_editor):
"""Rename legacy users."""
legacy_usernames_to_user_category_map = {
legacy_usernames_to_user_type_map = {
"DIODE_TO_NETBOX": "diode_to_netbox",
"NETBOX_TO_DIODE": "netbox_to_diode",
"DIODE": "diode",
}

User = apps.get_model("users", "User")
users = User.objects.filter(
username__in=legacy_usernames_to_user_category_map.keys(),
username__in=legacy_usernames_to_user_type_map.keys(),
groups__name="diode",
)

for user in users:
user_category = legacy_usernames_to_user_category_map.get(user.username)
user.username = get_diode_usernames().get(user_category)
user_type = legacy_usernames_to_user_type_map.get(user.username)
user.username = get_diode_usernames().get(user_type)
user.save()


Expand Down
35 changes: 25 additions & 10 deletions netbox_diode_plugin/plugin_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,35 @@

from netbox.plugins import get_plugin_config

__all__ = ("get_diode_usernames", "get_diode_username_for_user_category")
__all__ = (
"get_diode_user_types",
"get_diode_usernames",
"get_diode_username_for_user_type",
)


def get_diode_user_types():
"""Returns a list of diode user types."""
return "diode_to_netbox", "netbox_to_diode", "diode"


def get_diode_user_types_with_labels():
"""Returns a list of diode user types with labels."""
return (
("diode_to_netbox", "Diode to NetBox"),
("netbox_to_diode", "NetBox to Diode"),
("diode", "Diode"),
)


def get_diode_usernames():
"""Returns a dictionary of diode user categories and their configured usernames."""
diode_user_categories = ("diode_to_netbox", "netbox_to_diode", "diode")
"""Returns a dictionary of diode user types and their configured usernames."""
return {
user_category: get_plugin_config(
"netbox_diode_plugin", f"{user_category}_username"
)
for user_category in diode_user_categories
user_type: get_plugin_config("netbox_diode_plugin", f"{user_type}_username")
for user_type in get_diode_user_types()
}


def get_diode_username_for_user_category(user_category):
"""Returns a diode username for a given user category."""
return get_plugin_config("netbox_diode_plugin", f"{user_category}_username")
def get_diode_username_for_user_type(user_type):
"""Returns a diode username for a given user type."""
return get_plugin_config("netbox_diode_plugin", f"{user_type}_username")
38 changes: 38 additions & 0 deletions netbox_diode_plugin/templates/diode/setup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% extends 'generic/_base.html' %}
{% load i18n %}

{% block title %}{% trans "Setup" %}{% endblock %}

{% block content %}

<div class="alert alert-warning mt-3" role="alert">
<h4 class="alert-heading">{% trans "Important" %}</h4>
<p>{% trans "Diode is not currently configured and requires a set of users and API keys to be created before using Diode. If fields in this form are read-only, they have been pre-populated based on application settings that cannot be overwritten." %}</p>
<p>{% trans "Click the \"Create\" button to create the required Diode users and API keys and proceed." %}</p>
</div>

<div class="tab-pane show active" id="setup-form" role="tabpanel" aria-labelledby="setup-tab">
<form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
{% csrf_token %}

<div class="row">
<h2 class="col-9 offset-3">{% trans "Diode users and API Keys" %}</h2>
</div>

<div id="form_fields" hx-disinherit="hx-select hx-swap">
{% block form %}
{% include 'htmx/form.html' %}
{% endblock form %}
</div>

<div class="text-end my-3">
{% block buttons %}
<button type="submit" name="_update" class="btn btn-primary">
{% trans "Create" %}
</button>
{% endblock buttons %}
</div>
</form>
</div>
{% endblock content %}

Loading

0 comments on commit 962e2d5

Please sign in to comment.