diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6af69fa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,214 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +**/__pycache__/ +/netbox_secrets.egg-info diff --git a/.github/configuration.testing.py b/.github/configuration.testing.py deleted file mode 100644 index 80e563d..0000000 --- a/.github/configuration.testing.py +++ /dev/null @@ -1,38 +0,0 @@ -################################################################### -# This file serves as a base configuration for testing purposes # -# only. It is not intended for production use. # -################################################################### - -ALLOWED_HOSTS = ['*'] - -DATABASE = { - 'NAME': 'netbox', - 'USER': 'netbox', - 'PASSWORD': 'netbox', - 'HOST': 'localhost', - 'PORT': '', - 'CONN_MAX_AGE': 300, -} - -PLUGINS = [ - 'netbox_secrets', -] - -REDIS = { - 'tasks': { - 'HOST': 'localhost', - 'PORT': 6379, - 'PASSWORD': '', - 'DATABASE': 0, - 'SSL': False, - }, - 'caching': { - 'HOST': 'localhost', - 'PORT': 6379, - 'PASSWORD': '', - 'DATABASE': 1, - 'SSL': False, - } -} - -SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' diff --git a/.github/verify-bundles.sh b/.github/verify-bundles.sh index 4a3b403..7922e6e 100644 --- a/.github/verify-bundles.sh +++ b/.github/verify-bundles.sh @@ -8,7 +8,7 @@ echo "$PWD" -PROJECT_STATIC="$PWD/netbox-secrets/netbox_secrets/project-static" +PROJECT_STATIC="$PWD/netbox_secrets/project-static" DIST="$PROJECT_STATIC/dist/" # Bundle static assets. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..05f2ec8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + pull_request: + +# This ensures that previous jobs for the workflow are canceled when the ref is +# updated. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: misc + name: Checks syntax of our code + steps: + - uses: actions/checkout@v3 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Lint Code Base + uses: github/super-linter/slim@v4 + env: + DEFAULT_BRANCH: dev + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SUPPRESS_POSSUM: true + LINTER_RULES_PATH: / + VALIDATE_ALL_CODEBASE: false + VALIDATE_DOCKERFILE: false + VALIDATE_JSCPD: true + FILTER_REGEX_EXCLUDE: (.*/)?(configuration/.*) + PYTHON_BLACK_CONFIG_FILE: pyproject.toml + PYTHON_FLAKE8_CONFIG_FILE: pyproject.toml + PYTHON_ISORT_CONFIG_FILE: pyproject.toml + test: + strategy: + matrix: + test_cmd: + - ./test.sh feature + - ./test.sh snapshot + - ./test.sh latest + - ./test.sh v3.3.10 + fail-fast: false + runs-on: misc + name: Runs plugin tests + steps: + - id: git-checkout + name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: 16 + - run: yarn --cwd netbox_secrets/project-static + + - name: Check UI ESLint, TypeScript, and Prettier Compliance + run: yarn --cwd netbox_secrets/project-static validate + + - name: Validate Static Asset Integrity + run: bash .github/verify-bundles.sh + + - id: docker-test + name: Test the image + run: ${{ matrix.test_cmd }} diff --git a/.gitignore b/.gitignore index a51858b..acdbc5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,220 @@ -netbox_secrets.egg-info +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +**/__pycache__/ +/netbox_secrets.egg-info node_modules yarn.error /netbox_secrets/project-static/.cache/ /netbox_secrets/project-static/netbox/ -__pycache__ build dist -.idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..0838f44 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9216566 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a1a41ea --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/netbox-secrets.iml b/.idea/netbox-secrets.iml new file mode 100644 index 0000000..c410d0c --- /dev/null +++ b/.idea/netbox-secrets.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000..263079f --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,3 @@ +{ + "ignore": ["**/tests/**"] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..85ff6d0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +ARG NETBOX_VARIANT=v3.3 + +FROM registry.tangience.net/netbox/netbox:${NETBOX_VARIANT} + +USER root + +# Remove pre-installed plugin +RUN rm -rf /usr/local/lib/python3.10/site-packages/netbox_secrets + +RUN mkdir -pv /plugins/netbox-secrets +COPY . /plugins/netbox-secrets + +RUN python3 /plugins/netbox-secrets/setup.py develop +RUN cp -rf /plugins/netbox-secrets/netbox_secrets/ /usr/local/lib/python3.10/site-packages/netbox_secrets + +USER $USER diff --git a/configuration/configuration.py b/configuration/configuration.py new file mode 100644 index 0000000..cdc99a7 --- /dev/null +++ b/configuration/configuration.py @@ -0,0 +1,86 @@ +#### +## We recommend to not edit this file. +## Create separate files to overwrite the settings. +## See `extra.py` as an example. +#### + +from os import environ +from os.path import abspath, dirname + +# For reference see https://netbox.readthedocs.io/en/stable/configuration/ +# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py + +# Read secret from file +def _read_secret(secret_name, default=None): + try: + f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8') + except EnvironmentError: + return default + else: + with f: + return f.readline().strip() + + +_BASE_DIR = dirname(dirname(abspath(__file__))) + +######################### +# # +# Required settings # +# # +######################### + +# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write +# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. +# +# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] +ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ') + +# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: +# https://docs.djangoproject.com/en/stable/ref/settings/#databases +DATABASE = { + 'NAME': environ.get('DB_NAME', 'netbox'), # Database name + 'USER': environ.get('DB_USER', ''), # PostgreSQL username + 'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')), + # PostgreSQL password + 'HOST': environ.get('DB_HOST', 'localhost'), # Database server + 'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default) + 'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')}, + # Database connection SSLMODE + 'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')), + # Max database connection age + 'DISABLE_SERVER_SIDE_CURSORS': environ.get('DB_DISABLE_SERVER_SIDE_CURSORS', 'False').lower() == 'true', + # Disable the use of server-side cursors transaction pooling +} + +# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate +# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended +# to use two separate database IDs. +REDIS = { + 'tasks': { + 'HOST': environ.get('REDIS_HOST', 'localhost'), + 'PORT': int(environ.get('REDIS_PORT', 6379)), + 'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')), + 'DATABASE': int(environ.get('REDIS_DATABASE', 0)), + 'SSL': environ.get('REDIS_SSL', 'False').lower() == 'true', + 'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False').lower() == 'true', + }, + 'caching': { + 'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')), + 'PORT': int(environ.get('REDIS_CACHE_PORT', environ.get('REDIS_PORT', 6379))), + 'PASSWORD': _read_secret( + 'redis_cache_password', environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', '')) + ), + 'DATABASE': int(environ.get('REDIS_CACHE_DATABASE', 1)), + 'SSL': environ.get('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False')).lower() == 'true', + 'INSECURE_SKIP_TLS_VERIFY': environ.get( + 'REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False') + ).lower() + == 'true', + }, +} + +# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. +# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and +# symbols. NetBox will not run without this defined. For more information, see +# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY +SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', '')) diff --git a/configuration/logging.py b/configuration/logging.py new file mode 100644 index 0000000..86914ae --- /dev/null +++ b/configuration/logging.py @@ -0,0 +1,11 @@ +# Remove first comment(#) on each line to implement this working logging example. +# Add LOGLEVEL environment variable to netbox if you use this example & want a different log level. +from os import environ + +# Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO. +LOGLEVEL = environ.get("LOGLEVEL", "INFO") + +LOGGING = { + "version": 1, + "disable_existing_loggers": True, +} diff --git a/configuration/plugins.py b/configuration/plugins.py new file mode 100644 index 0000000..58f9797 --- /dev/null +++ b/configuration/plugins.py @@ -0,0 +1,13 @@ +# Add your plugins and plugin settings here. +# Of course uncomment this file out. + +# To learn how to build images with your required plugins +# See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins + +PLUGINS = [ + "netbox_secrets", +] + +PLUGINS_CONFIG = { # type: ignore + "netbox_secrets": {}, +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9b82488 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.4' + +services: + netbox: + build: + dockerfile: Dockerfile + context: . + args: + NETBOX_VARIANT: ${NETBOX_VARIANT} + depends_on: + - postgres + - redis + env_file: env/netbox.env + volumes: + - ./configuration:/etc/netbox/config:z,ro + + # postgres + postgres: + image: postgres:14-alpine + env_file: env/postgres.env + + # redis + redis: + image: redis:6-alpine + command: + - sh + - -c # this is to evaluate the $REDIS_PASSWORD from the env + - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose + env_file: env/redis.env \ No newline at end of file diff --git a/env/netbox.env b/env/netbox.env new file mode 100644 index 0000000..e39dab5 --- /dev/null +++ b/env/netbox.env @@ -0,0 +1,23 @@ +ALLOWED_HOSTS=* +CORS_ORIGIN_ALLOW_ALL=true +DB_HOST=postgres +DB_NAME=netbox +DB_PASSWORD=J5brHrAXFLQSif0K +DB_USER=netbox +DEBUG=true +ENFORCE_GLOBAL_UNIQUE=true +LOGIN_REQUIRED=false +GRAPHQL_ENABLED=true +MAX_PAGE_SIZE=1000 +MEDIA_ROOT=/opt/netbox/netbox/media +REDIS_DATABASE=0 +REDIS_HOST=redis +REDIS_INSECURE_SKIP_TLS_VERIFY=false +REDIS_PASSWORD=H733Kdjndks81 +SECRET_KEY=r8OwDznj!!dciP9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj +SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 +SUPERUSER_EMAIL=admin@example.com +SUPERUSER_NAME=admin +SUPERUSER_PASSWORD=admin +STARTUP_SCRIPTS=false +WEBHOOKS_ENABLED=true \ No newline at end of file diff --git a/env/postgres.env b/env/postgres.env new file mode 100644 index 0000000..045e64b --- /dev/null +++ b/env/postgres.env @@ -0,0 +1,3 @@ +POSTGRES_DB=netbox +POSTGRES_PASSWORD=J5brHrAXFLQSif0K +POSTGRES_USER=netbox \ No newline at end of file diff --git a/env/redis.env b/env/redis.env new file mode 100644 index 0000000..dcb03dd --- /dev/null +++ b/env/redis.env @@ -0,0 +1 @@ +REDIS_PASSWORD=H733Kdjndks81 \ No newline at end of file diff --git a/netbox_secrets/api/serializers.py b/netbox_secrets/api/serializers.py index 36d54c3..5338d9d 100644 --- a/netbox_secrets/api/serializers.py +++ b/netbox_secrets/api/serializers.py @@ -14,7 +14,7 @@ # Secrets # -class SecretRoleSerializer(NestedGroupModelSerializer): +class SecretRoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_secrets-api:secretrole-detail') secret_count = serializers.IntegerField(read_only=True) diff --git a/netbox_secrets/api/views.py b/netbox_secrets/api/views.py index 216e11c..d9dd19b 100644 --- a/netbox_secrets/api/views.py +++ b/netbox_secrets/api/views.py @@ -11,8 +11,7 @@ from rest_framework.routers import APIRootView from rest_framework.viewsets import ViewSet -from extras.api.views import CustomFieldViewSet -from netbox.api.viewsets import ModelViewSet +from netbox.api.viewsets import NetBoxModelViewSet from netbox_secrets import filtersets from netbox_secrets.exceptions import InvalidKey from netbox_secrets.models import Secret, SecretRole, SessionKey, UserKey @@ -37,7 +36,7 @@ def get_view_name(self): # Secret Roles # -class SecretRoleViewSet(CustomFieldViewSet): +class SecretRoleViewSet(NetBoxModelViewSet): queryset = SecretRole.objects.annotate( secret_count=count_related(Secret, 'role') ) @@ -49,7 +48,7 @@ class SecretRoleViewSet(CustomFieldViewSet): # Secrets # -class SecretViewSet(ModelViewSet): +class SecretViewSet(NetBoxModelViewSet): queryset = Secret.objects.prefetch_related('role', 'tags') serializer_class = serializers.SecretSerializer filterset_class = filtersets.SecretFilterSet diff --git a/netbox_secrets/forms/secrets.py b/netbox_secrets/forms/secrets.py index 1ccd627..7f389dd 100644 --- a/netbox_secrets/forms/secrets.py +++ b/netbox_secrets/forms/secrets.py @@ -269,10 +269,6 @@ class UserKeyForm(forms.ModelForm): class Meta: model = UserKey fields = ['public_key'] - help_texts = { - 'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. " - "Please note that passphrase-protected keys are not supported.", - } labels = { 'public_key': '' } diff --git a/netbox_secrets/migrations/0002_populate_userkeys.py b/netbox_secrets/migrations/0002_populate_userkeys.py index 3f5cb48..7fe0dd7 100644 --- a/netbox_secrets/migrations/0002_populate_userkeys.py +++ b/netbox_secrets/migrations/0002_populate_userkeys.py @@ -3,7 +3,11 @@ def populate_userkeys(apps, schema_editor): """Populate the UserKey model with data from the SecretStore model.""" - UserKeyOld = apps.get_model('netbox_secretstore', 'UserKey') + try: + UserKeyOld = apps.get_model('netbox_secretstore', 'UserKey') + except LookupError: + # Skip if the old model doesn't exist + return UserKey = apps.get_model('netbox_secrets', 'UserKey') # Retrieve the necessary data from SecretStore objects diff --git a/netbox_secrets/migrations/0003_populate_secretroles.py b/netbox_secrets/migrations/0003_populate_secretroles.py index 04db7ab..e6f42ae 100644 --- a/netbox_secrets/migrations/0003_populate_secretroles.py +++ b/netbox_secrets/migrations/0003_populate_secretroles.py @@ -3,7 +3,11 @@ def populate_secretroles(apps, schema_editor): """Populate the SecretRole model with data from the SecretStore model.""" - SecretRoleOld = apps.get_model('netbox_secretstore', 'SecretRole') + try: + SecretRoleOld = apps.get_model('netbox_secretstore', 'SecretRole') + except LookupError: + # Skip if the old model doesn't exist + return SecretRole = apps.get_model('netbox_secrets', 'SecretRole') # Retrieve the necessary data from SecretStore objects diff --git a/netbox_secrets/migrations/0004_populate_secrets.py b/netbox_secrets/migrations/0004_populate_secrets.py index 203815c..79cb434 100644 --- a/netbox_secrets/migrations/0004_populate_secrets.py +++ b/netbox_secrets/migrations/0004_populate_secrets.py @@ -3,7 +3,11 @@ def populate_secrets(apps, schema_editor): """Populate the Secret model with data from the SecretStore model.""" - SecretOld = apps.get_model('netbox_secretstore', 'Secret') + try: + SecretOld = apps.get_model('netbox_secretstore', 'Secret') + except LookupError: + # Skip if the old model doesn't exist + return Secret = apps.get_model('netbox_secrets', 'Secret') # Retrieve the necessary data from SecretStore objects diff --git a/netbox_secrets/models/secrets.py b/netbox_secrets/models/secrets.py index 8ecd6a5..b60a600 100644 --- a/netbox_secrets/models/secrets.py +++ b/netbox_secrets/models/secrets.py @@ -4,6 +4,7 @@ from Crypto.PublicKey import RSA from Crypto.Util import strxor from django.conf import settings +from django.utils.translation import gettext_lazy as _ from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation @@ -51,7 +52,8 @@ class UserKey(models.Model): editable=False ) public_key = models.TextField( - verbose_name='RSA public key' + verbose_name='RSA public key', + help_text=_('Enter your public RSA key. Keep the private one with you; you will need it for decryption. Please note that passphrase-protected keys are not supported.') ) master_key_cipher = models.BinaryField( max_length=512, @@ -332,6 +334,10 @@ def __str__(self): def get_absolute_url(self): return reverse('plugins:netbox_secrets:secret', args=[self.pk]) + @classmethod + def get_prerequisite_models(cls): + return [SecretRole] + def to_csv(self): return ( f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}', diff --git a/netbox_secrets/navigation.py b/netbox_secrets/navigation.py index 0b4efd1..16b8c9b 100644 --- a/netbox_secrets/navigation.py +++ b/netbox_secrets/navigation.py @@ -1,40 +1,50 @@ from extras.plugins import PluginMenuItem, PluginMenuButton menu_items = ( - PluginMenuItem(link_text="User Key", link="plugins:netbox_secrets:userkey"), + PluginMenuItem( + link_text="User Key", + link="plugins:netbox_secrets:userkey", + permissions=["netbox_secrets.view_userkey"], + ), PluginMenuItem( link_text="Secret Roles", link="plugins:netbox_secrets:secretrole_list", + permissions=["netbox_secrets.view_secretrole"], buttons=( PluginMenuButton( link="plugins:netbox_secrets:secretrole_add", title="Add Secret Role", icon_class="mdi mdi-plus-thick", color="green", + permissions=["netbox_secrets.add_secretrole"], ), PluginMenuButton( link="plugins:netbox_secrets:secretrole_import", title="Import Secret Role", icon_class="mdi mdi-upload", color="teal", + permissions=["netbox_secrets.add_secretrole"], ), ), ), PluginMenuItem( link_text="Secrets", link="plugins:netbox_secrets:secret_list", + permissions=["netbox_secrets.view_secret"], buttons=( PluginMenuButton( link="plugins:netbox_secrets:secret_add", title="Add Secret", icon_class="mdi mdi-plus-thick", color="green", + permissions=["netbox_secrets.add_secret"], ), PluginMenuButton( link="plugins:netbox_secrets:secret_import", title="Import Secret", icon_class="mdi mdi-upload", color="teal", + permissions=["netbox_secrets.add_secret"], ), ), ), diff --git a/netbox_secrets/tests/test_api.py b/netbox_secrets/tests/test_api.py index e325947..5d0fbb1 100644 --- a/netbox_secrets/tests/test_api.py +++ b/netbox_secrets/tests/test_api.py @@ -75,8 +75,7 @@ class SecretTest( ): model = Secret view_namespace = 'plugins-api:netbox_secrets' - brief_fields = ['assigned_object', 'assigned_object_id', 'assigned_object_type', 'created', 'custom_fields', - 'display', 'hash', 'id', 'last_updated', 'name', 'plaintext', 'role', 'tags', 'url'] + brief_fields = ['display', 'id', 'name', 'url'] def setUp(self): super().setUp() diff --git a/pre-commit b/pre-commit new file mode 100755 index 0000000..25ddf23 --- /dev/null +++ b/pre-commit @@ -0,0 +1,32 @@ +#!/bin/sh +# Create a link to this file at .git/hooks/pre-commit + +exec 1>&2 + +EXIT=0 +RED='\033[0;31m' +NOCOLOR='\033[0m' + +echo "Running black..." +black . +if [ $? != 0 ]; then + EXIT=1 +fi + +echo "Running isort..." +isort . +if [ $? != 0 ]; then + EXIT=1 +fi + +echo "Running flake8..." +flake8 . +if [ $? != 0 ]; then + EXIT=1 +fi + +if [ $EXIT != 0 ]; then + printf "${RED}COMMIT FAILED${NOCOLOR}\n" +fi + +exit $EXIT diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..74dd1fd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +# See PEP 518 for the spec of this file +# https://www.python.org/dev/peps/pep-0518/ + +[tool.black] +line-length = 120 +target_version = ['py38', 'py39', 'py310'] +skip-string-normalization = true + +[tool.isort] +profile = "black" + +[tool.pylint] +max-line-length = 120 + +[tool.pyright] +include = ["netbox_secrets"] +exclude = [ + "**/node_modules", + "**/__pycache__", +] +reportMissingImports = true +reportMissingTypeStubs = false diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..6bbaf9d --- /dev/null +++ b/test.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Runs the NetBox plugin unit tests +# Usage: +# ./test.sh latest +# ./test.sh v2.9.7 +# ./test.sh develop-2.10 + +# exit when a command exits with an exit code != 0 +set -e + +# NETBOX_VARIANT is used by `Dockerfile` to determine the tag +NETBOX_VARIANT="${1-latest}" + +# The docker compose command to use +doco="docker compose --file docker-compose.yml" + +test_netbox_unit_tests() { + echo "⏱ Running NetBox Unit Tests" + $doco run --rm netbox python manage.py test netbox_secrets +} + +test_cleanup() { + echo "💣 Cleaning Up" + $doco down -v + $doco rm -fsv + docker image rm docker.io/library/netbox-secrets-netbox || echo '' +} + +export NETBOX_VARIANT=${NETBOX_VARIANT} + +echo "🐳🐳🐳 Start testing '${NETBOX_VARIANT}'" + +# Make sure the cleanup script is executed +trap test_cleanup EXIT ERR +test_netbox_unit_tests + +echo "🐳🐳🐳 Done testing '${NETBOX_VARIANT}'"