├── src-api
├── lib
│ ├── api
│ │ ├── __init__.py
│ │ └── dependencies.py
│ ├── pda
│ │ ├── __init__.py
│ │ └── api
│ │ │ ├── services
│ │ │ ├── __init__.py
│ │ │ └── mail.py
│ │ │ └── __init__.py
│ ├── util
│ │ └── __init__.py
│ ├── services
│ │ ├── __init__.py
│ │ ├── twilio
│ │ │ ├── __init__.py
│ │ │ └── messaging.py
│ │ ├── microsoft
│ │ │ ├── __init__.py
│ │ │ └── teams.py
│ │ └── zabbix.py
│ ├── jinja.py
│ ├── config
│ │ ├── logging.py
│ │ ├── paths.py
│ │ ├── server.py
│ │ ├── celery.py
│ │ ├── notifications.py
│ │ ├── tasks.py
│ │ ├── db.py
│ │ ├── __init__.py
│ │ ├── mail.py
│ │ ├── api.py
│ │ ├── app.py
│ │ └── services.py
│ ├── security.py
│ ├── enums.py
│ └── prometheus.py
├── models
│ ├── __init__.py
│ ├── db
│ │ ├── audits.py
│ │ ├── __init__.py
│ │ ├── system.py
│ │ ├── tenants.py
│ │ ├── crypto.py
│ │ ├── servers.py
│ │ ├── views.py
│ │ └── tasks.py
│ ├── api
│ │ ├── __init__.py
│ │ └── auth.py
│ └── base.py
├── tasks
│ ├── __init__.py
│ └── pda.py
├── middleware
│ └── __init__.py
├── templates
│ ├── shared
│ │ └── mail
│ │ │ ├── footer.jinja2
│ │ │ └── header.jinja2
│ └── pda
│ │ └── alert
│ │ ├── subject.jinja2
│ │ ├── body_text.jinja2
│ │ └── body_html.jinja2
├── __init__.py
├── routers
│ ├── api.py
│ ├── v1
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ └── mail.py
│ │ ├── auth.py
│ │ └── __init__.py
│ ├── __init__.py
│ ├── root.py
│ └── dev.py
├── worker.py
└── api.py
├── .github
├── FUNDING.yml
├── FUNDING.md
├── CHANGELOG.md
├── ACKNOWLEDGMENTS.md
├── CODEOWNERS.md
├── SUPPORT.md
├── CONTRIBUTORS.md
├── AUTHORS.md
├── CONTRIBUTING.md
├── dependabot.yml
├── workflows
│ ├── deployment-testing.yml
│ ├── lock.yml
│ ├── project-release-test.yml
│ ├── image-build.yml
│ ├── stale.yml
│ ├── python-build.yml
│ ├── project-release.yml
│ ├── codeql-analysis.yml
│ └── mega-linter.yml
├── PULL_REQUEST_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── housekeeping.yaml
│ ├── documentation_change.yaml
│ ├── bug_report.yaml
│ └── feature_request.yaml
├── SECURITY.md
├── CODE_OF_CONDUCT.md
└── labels.yml
├── docs
├── wiki
│ ├── testing
│ │ └── README.md
│ ├── project
│ │ ├── releases.md
│ │ ├── roadmap.md
│ │ ├── milestones.md
│ │ └── README.md
│ ├── deployment
│ │ ├── docker
│ │ │ ├── docker.md
│ │ │ ├── docker-swarm.md
│ │ │ ├── docker-compose.md
│ │ │ └── README.md
│ │ ├── kubernetes
│ │ │ ├── k3s.md
│ │ │ ├── kubeadm.md
│ │ │ ├── rancher.md
│ │ │ ├── minikube.md
│ │ │ ├── helm-charts.md
│ │ │ └── README.md
│ │ ├── bare-metal
│ │ │ ├── bsd
│ │ │ │ ├── freebsd.md
│ │ │ │ └── README.md
│ │ │ ├── linux
│ │ │ │ ├── alma-linux.md
│ │ │ │ ├── arch-linux.md
│ │ │ │ ├── alpine-linux.md
│ │ │ │ ├── centos-linux.md
│ │ │ │ ├── debian-linux.md
│ │ │ │ ├── fedora-linux.md
│ │ │ │ ├── oracle-linux.md
│ │ │ │ ├── rocky-linux.md
│ │ │ │ ├── ubuntu-linux.md
│ │ │ │ ├── opensuse-linux.md
│ │ │ │ ├── redhat-enterprise-linux.md
│ │ │ │ ├── suse-linux-enterprise-server.md
│ │ │ │ └── README.md
│ │ │ ├── macos
│ │ │ │ └── README.md
│ │ │ ├── windows
│ │ │ │ └── README.md
│ │ │ ├── solaris
│ │ │ │ └── README.md
│ │ │ └── README.md
│ │ └── README.md
│ ├── configuration
│ │ ├── settings
│ │ │ ├── runtime-settings.md
│ │ │ └── README.md
│ │ └── README.md
│ ├── support
│ │ └── README.md
│ ├── README.md
│ └── contributing
│ │ └── labeling-standards.md
├── announcements
│ ├── 2023-03-19-new-contribution-policy.md
│ ├── 2023-03-10-docker-images-and-repository-branches.md
│ ├── 2022-12-09-docker-hub-repository-moved.md
│ ├── 2023-03-11-release-v0.4.0.md
│ ├── 2024-01-31-release-0.4.2.md
│ └── 2023-04-11-release-0.4.1.md
└── README.md
├── .env.tpl
├── .env.local.tpl
├── todo.md
├── deploy
└── docker
│ ├── scripts
│ └── entrypoint-web.sh
│ ├── nginx
│ ├── conf
│ │ ├── web.conf
│ │ └── proxy.conf.template
│ └── certs
│ │ ├── proxy.crt
│ │ └── proxy.key
│ ├── api
│ └── Dockerfile
│ └── web
│ └── Dockerfile
├── sync-exclusions.txt
├── .dockerignore
├── .gitignore
├── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.dev.yml
└── docker-compose.yml
/src-api/lib/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/lib/pda/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/lib/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/models/db/audits.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/tasks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/lib/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/middleware/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/models/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/lib/pda/api/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/lib/services/twilio/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-api/templates/shared/mail/footer.jinja2:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [AzorianSolutions]
--------------------------------------------------------------------------------
/.github/FUNDING.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Funding
4 |
--------------------------------------------------------------------------------
/src-api/templates/pda/alert/subject.jinja2:
--------------------------------------------------------------------------------
1 | {{ title }}
--------------------------------------------------------------------------------
/.github/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Change Log
4 |
--------------------------------------------------------------------------------
/.github/ACKNOWLEDGMENTS.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Acknowledgments
4 |
--------------------------------------------------------------------------------
/src-api/templates/pda/alert/body_text.jinja2:
--------------------------------------------------------------------------------
1 | {{ title }}
2 | {{ message_plain }}
--------------------------------------------------------------------------------
/docs/wiki/testing/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Testing Guide
4 |
5 | Coming eventually!
--------------------------------------------------------------------------------
/docs/wiki/project/releases.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Project Releases
4 |
5 | Coming eventually!
--------------------------------------------------------------------------------
/docs/wiki/project/roadmap.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Project Roadmap
4 |
5 | Coming eventually!
--------------------------------------------------------------------------------
/docs/wiki/project/milestones.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Project Milestones
4 |
5 | Coming eventually!
--------------------------------------------------------------------------------
/docs/wiki/deployment/docker/docker.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Docker Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/docker/docker-swarm.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Docker Swarm Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/kubernetes/k3s.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Kubernetes K3S Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/bsd/freebsd.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## FreeBSD Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/alma-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Alma Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/arch-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Arch Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/docker/docker-compose.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Docker Compose Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/kubernetes/kubeadm.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Kubernetes Kubeadm Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/kubernetes/rancher.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Kubernetes Rancher Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/alpine-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Alpine Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/centos-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## CentOS Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/debian-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Debian Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/fedora-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Fedora Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/oracle-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Oracle Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/rocky-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Rocky Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/ubuntu-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Ubuntu Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/macos/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## macOS Bare Metal Installation Guide
4 |
5 | Good luck!
--------------------------------------------------------------------------------
/docs/wiki/deployment/kubernetes/minikube.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Kubernetes Minikube Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/opensuse-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## openSUSE Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/windows/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Windows Bare Metal Installation Guide
4 |
5 | Good luck!
--------------------------------------------------------------------------------
/docs/wiki/deployment/kubernetes/helm-charts.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Kubernetes Helm Charts Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/solaris/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Solaris Bare Metal Deployment Guides
4 |
5 | Coming soon!
6 |
--------------------------------------------------------------------------------
/src-api/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, unicode_literals
2 | from .worker import app as celery_app
3 | __all__ = ('celery_app',)
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/redhat-enterprise-linux.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Redhat Enterprise Linux Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/src-api/lib/jinja.py:
--------------------------------------------------------------------------------
1 |
2 | class JinjaFilters:
3 |
4 | @staticmethod
5 | def implement_filters(filters: dict):
6 | return filters
7 |
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/suse-linux-enterprise-server.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## SUSE Linux Enterprise Server Deployment Guide
4 |
5 | Coming soon!
--------------------------------------------------------------------------------
/.env.tpl:
--------------------------------------------------------------------------------
1 | PYTHONPATH=/srv/app/src
2 | PDA_DEBUG=0
3 | PDA_ENV=prod
4 | PDA_SERVICE_IP=10.0.0.1
5 | PDA_HOSTNAME=prod.powerdnsadmin.org
6 | PDA_MYSQL_ROOT_PASSWORD=CHANGE-TO-SECURE-PASSWORD
7 | PDA_MYSQL_DATABASE=pda
--------------------------------------------------------------------------------
/.env.local.tpl:
--------------------------------------------------------------------------------
1 | PYTHONPATH=/srv/app/src
2 | PDA_DEBUG=1
3 | PDA_ENV=local
4 | PDA_SERVICE_IP=127.0.0.1
5 | PDA_HOSTNAME=prod.powerdnsadmin.org
6 | PDA_MYSQL_ROOT_PASSWORD=CHANGE-TO-SECURE-PASSWORD
7 | PDA_MYSQL_DATABASE=pda
--------------------------------------------------------------------------------
/.github/CODEOWNERS.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Code Owners
4 |
5 | - Azorian Solutions LLC <help@azorian.solutions>
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/bsd/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## BSD Bare Metal Deployment Guides
4 |
5 | - [FreeBSD](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/bsd/freebsd.md)
6 |
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | # PDA Todo List
2 |
3 | ## Problems
4 |
5 | - Nothing to address. Woo Hoo!
6 |
7 | ## High Priority
8 |
9 | - Nothing to address. Woo Hoo!
10 |
11 | ## Low Priority
12 |
13 | - Nothing to address. Woo Hoo!
--------------------------------------------------------------------------------
/src-api/lib/config/logging.py:
--------------------------------------------------------------------------------
1 | from models.base import BaseConfig
2 |
3 |
4 | class LoggingConfig(BaseConfig):
5 | """A model that represents a configuration hierarchy for logging settings."""
6 | level: str = 'info'
7 |
--------------------------------------------------------------------------------
/deploy/docker/scripts/entrypoint-web.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | npm install
4 |
5 | if [ "${PDA_ENV}" == "local" ]; then
6 | vite --host 0.0.0.0 --port 8000
7 | else
8 | npm run build
9 | nginx -g "daemon off;"
10 | fi
--------------------------------------------------------------------------------
/sync-exclusions.txt:
--------------------------------------------------------------------------------
1 | __pycache__
2 | .app.env
3 | .env
4 | celerybeat-schedule
5 | config.yml
6 | notifications.yml
7 | schedules.yml
8 | src-ui/.env
9 | src-ui/.env.local
10 | src-ui/package-lock.json
11 | src-ui/dist
12 | src-ui/node_modules
--------------------------------------------------------------------------------
/docs/announcements/2023-03-19-new-contribution-policy.md:
--------------------------------------------------------------------------------
1 | Please be aware that the project has implemented a new contribution policy, effective immediately!
2 |
3 | Please read the [new policy here](https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/master/docs/CONTRIBUTING.md).
--------------------------------------------------------------------------------
/src-api/lib/config/paths.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from models.base import BaseConfig
3 |
4 |
5 | class PathsConfig(BaseConfig):
6 | """A model that represents a configuration hierarchy for file system paths."""
7 | templates: Union[str, list[str]] = 'src/templates'
8 |
--------------------------------------------------------------------------------
/src-api/models/db/__init__.py:
--------------------------------------------------------------------------------
1 | import models.db.acl
2 | import models.db.audits
3 | import models.db.auth
4 | import models.db.crypto
5 | import models.db.servers
6 | import models.db.system
7 | import models.db.tasks
8 | import models.db.tenants
9 | import models.db.views
10 | import models.db.zones
--------------------------------------------------------------------------------
/src-api/routers/api.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from routers.root import router_responses
3 | from routers.v1 import router as v1_router
4 |
5 | router = APIRouter(
6 | prefix='/api',
7 | responses=router_responses,
8 | )
9 |
10 | # Setup descendent routers
11 | router.include_router(v1_router)
12 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | ___*
2 | __pycache__
3 | .DS_Store
4 | .git
5 | .github
6 | .idea
7 | *.egg-info
8 | celerybeat-schedule
9 | config.yml
10 | config.*.yml
11 | notifications.yml
12 | notifications.*.yml
13 | schedules.yml
14 | schedules.*.yml
15 | docker-compose.yml
16 | docker-compose.*.yml
17 | node_modules
18 | venv
19 | var
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ___*
2 | __pycache__
3 | .DS_Store
4 | .env
5 | .*.env
6 | .idea
7 | *.egg-info
8 | celerybeat-schedule
9 | config.yml
10 | notifications.yml
11 | schedules.yml
12 | docker/nginx/certs/*
13 | src-ui/node_modules/
14 | src-ui/build/*
15 | src-ui/dist/*
16 | src-ui/package-lock.json
17 | node_modules
18 | venv
19 | var
--------------------------------------------------------------------------------
/src-api/routers/v1/services/__init__.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from routers.root import router_responses
3 | from routers.v1.services import mail
4 |
5 | router = APIRouter(
6 | prefix='/services',
7 | responses=router_responses,
8 | )
9 |
10 | # Setup descendent routers
11 | router.include_router(mail.router)
12 |
--------------------------------------------------------------------------------
/deploy/docker/nginx/conf/web.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8000;
3 | server_name _;
4 | access_log /var/log/nginx/web.access.log combined;
5 | error_log /var/log/nginx/web.error.log error;
6 | root /srv/app/dist;
7 | location / {
8 | try_files $uri $uri/ /index.html;
9 | index index.html;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs/wiki/configuration/settings/runtime-settings.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Configuration Guide
4 |
5 | ### Runtime Configuration Guide
6 |
7 | The configuration settings listed in this section are used during runtime by various application features. Configuration
8 | settings are only placed here if the setting isn't required to bootstrap the application during initialization.
9 |
10 | **More coming soon!**
--------------------------------------------------------------------------------
/docs/wiki/deployment/docker/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Docker Deployment Guides
4 |
5 | - [Docker](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/docker/docker.md)
6 | - [Docker Compose](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/docker/docker-compose.md)
7 | - [Docker Swarm](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/docker/docker-swarm.md)
8 |
--------------------------------------------------------------------------------
/src-api/lib/config/server.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from models.base import BaseConfig
3 |
4 |
5 | class ServerConfig(BaseConfig):
6 | """A model that represents a configuration hierarchy for the HTTP server."""
7 |
8 | class MiddlewareConfig(BaseConfig):
9 | name: str
10 | config: Union[dict, None] = None
11 |
12 | proxy_root: str = '/'
13 | middleware: list[MiddlewareConfig] = []
14 |
--------------------------------------------------------------------------------
/src-api/lib/config/celery.py:
--------------------------------------------------------------------------------
1 | from models.base import BaseConfig
2 |
3 |
4 | class CeleryConfig(BaseConfig):
5 | """A model that represents a configuration hierarchy for the Celery app."""
6 |
7 | class BrokerConfig(BaseConfig):
8 | url: str = 'redis://redis:6379/0'
9 |
10 | class BackendConfig(BaseConfig):
11 | url: str = 'redis://redis:6379/0'
12 |
13 | broker: BrokerConfig
14 | backend: BackendConfig
15 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | **Looking to help?** Try taking a look at the project's
4 | [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md).
5 |
6 | ## [Support Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/support/README.md)
7 |
8 | Please see the project's [Support Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/support/README.md)
9 | for information on how to obtain support for the project.
--------------------------------------------------------------------------------
/.github/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Contributors
4 |
5 | This is a list of people / organizations that are currently contributing to the project in
6 | some way.
7 |
8 | This does not necessarily list everyone who has contributed code.
9 |
10 | To see the full list of contributors, see the revision history in
11 | source control.
12 |
13 | - Azorian Solutions LLC <help@azorian.solutions>
--------------------------------------------------------------------------------
/.github/AUTHORS.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Authors
4 |
5 | This is the list of the PDA application's significant contributors.
6 |
7 | This does not necessarily list everyone who has contributed code,
8 | especially since many employees of one corporation may be contributing.
9 |
10 | To see the full list of contributors, see the revision history in
11 | source control.
12 |
13 | - Azorian Solutions LLC <help@azorian.solutions>
--------------------------------------------------------------------------------
/src-api/lib/config/notifications.py:
--------------------------------------------------------------------------------
1 | from models.base import BaseConfig
2 |
3 |
4 | class NotificationsConfig(BaseConfig):
5 | """A model that represents a configuration hierarchy for notification settings."""
6 |
7 | class NotificationServiceConfig(BaseConfig):
8 | """Provides an abstract class for notification services to inherent from."""
9 | enabled: bool = False
10 |
11 | mail: NotificationServiceConfig
12 | microsoft_teams: NotificationServiceConfig
13 | twilio: NotificationServiceConfig
14 |
--------------------------------------------------------------------------------
/deploy/docker/api/Dockerfile:
--------------------------------------------------------------------------------
1 | ########################################
2 | ############ PDA API Image #############
3 | ########################################
4 | FROM python:3.12
5 |
6 | WORKDIR /srv/app
7 |
8 | # Copy the Python source files into the container
9 | COPY ./src-api ./src
10 |
11 | # Copy the Python pip requuirements into the container
12 | COPY ./requirements.txt ./requirements.txt
13 |
14 | # Install Python dependencies
15 | RUN python3 -m pip install --upgrade pip && python3 -m pip install --no-cache-dir -r requirements.txt
16 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | **Looking for help?** Try taking a look at the project's
4 | [Support Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/support/README.md) or joining
5 | our [Discord Server](https://discord.powerdnsadmin.org).
6 |
7 | ## [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md)
8 |
9 | Please see the project's [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md)
10 | for information on how to contribute to the project.
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | updates:
4 | - package-ecosystem: npm
5 | target-branch: dev
6 | directory: /
7 | schedule:
8 | interval: daily
9 | ignore:
10 | - dependency-name: "*"
11 | update-types: [ "version-update:semver-major" ]
12 | labels:
13 | - 'feature / dependency'
14 | - package-ecosystem: pip
15 | target-branch: dev
16 | directory: /
17 | schedule:
18 | interval: daily
19 | ignore:
20 | - dependency-name: "*"
21 | update-types: [ "version-update:semver-major" ]
22 | labels:
23 | - 'feature / dependency'
24 |
--------------------------------------------------------------------------------
/.github/workflows/deployment-testing.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Deployment Testing
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - '-dev'
8 | - '-main'
9 | - '-dependabot/**'
10 | - '-feature/**'
11 | - '-issue/**'
12 | - '-release/**'
13 | tags:
14 | - '-v*.*.*'
15 | jobs:
16 | deployment_test_ubuntu_linux_2204:
17 | runs-on: [self-hosted, linux, x64]
18 | environment: testing
19 | concurrency:
20 | group: deployment_test_ubuntu_linux_2204
21 | cancel-in-progress: true
22 | steps:
23 | - name: Repository Checkout
24 | uses: actions/checkout@v3
--------------------------------------------------------------------------------
/docs/wiki/deployment/kubernetes/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Kubernetes Deployment Guides
4 |
5 | - [Helm Charts](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/kubernetes/helm-charts.md)
6 | - [K3s](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/kubernetes/k3s.md)
7 | - [Kubeadm](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/kubernetes/kubeadm.md)
8 | - [Minikube](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/kubernetes/minikube.md)
9 | - [Rancher](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/kubernetes/rancher.md)
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
10 | ### Fixes: #1234
11 |
12 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiosqlite==0.21.0
2 | asyncio==4.0.0
3 | asyncmy==0.2.10
4 | celery[redis]==5.5.3
5 | cryptography==46.0.3
6 | email-validator==2.3.0
7 | fastapi==0.122.0
8 | itsdangerous==2.2.0
9 | jinja2==3.1.6
10 | jsonpickle==4.1.1
11 | loguru==0.7.3
12 | openpyxl==3.1.5
13 | pandas==2.3.3
14 | passlib==1.7.4
15 | prometheus-fastapi-instrumentator==7.1.0
16 | pydantic==2.12.5
17 | pydantic-settings==2.12.0
18 | pymysql==1.1.2
19 | python-dotenv==1.2.1
20 | python-jose==3.5.0
21 | python-multipart==0.0.20
22 | pyyaml==6.0.3
23 | redis==5.2.1 # This lower version is currently required by dependencies
24 | requests==2.32.5
25 | sqlalchemy[asyncio]==2.0.44
26 | twilio==9.8.7
27 | urllib3==2.5.0
28 | uvicorn==0.38.0
--------------------------------------------------------------------------------
/src-api/templates/pda/alert/body_html.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% include 'shared/mail/header.jinja2' %}
5 |
6 |
7 |
8 | |
9 | {{ title }}
10 | |
11 |
12 |
13 |
14 | {{ message_html }}
15 | {% include 'shared/mail/footer.jinja2' %}
16 |
17 |
--------------------------------------------------------------------------------
/src-api/lib/config/tasks.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from models.base import BaseConfig
3 |
4 |
5 | class TaskSchedule(BaseConfig):
6 | enabled: bool = True
7 | key: Union[str, None] = None
8 | name: str
9 | task: str
10 | at: Union[list[str], str, None] = None
11 | args: Union[list, None] = None
12 | kwargs: Union[dict, None] = None
13 | options: Union[dict, None] = None
14 |
15 |
16 | class TasksConfig(BaseConfig):
17 | """A model that represents a configuration hierarchy for the pda."""
18 |
19 | class TaskSchedulerConfig(BaseConfig):
20 | tick_interval: int = 30
21 | max_schedule_lifetime: int = 30
22 |
23 | enabled: bool = True
24 | scheduler: TaskSchedulerConfig
25 |
--------------------------------------------------------------------------------
/src-api/routers/__init__.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 |
3 |
4 | def install_routers(app: FastAPI) -> None:
5 | """Attach local and global routers"""
6 | from app import config
7 | from lib.config.app import EnvironmentEnum
8 | from routers import api, dev, root
9 | from routers import v1
10 |
11 | # Attach API router to root router
12 | # root.router.include_router(api.router)
13 |
14 | # Attach API V1 router to root router
15 | root.router.include_router(v1.router)
16 |
17 | # Dev Router
18 | if config.app.environment.name in (EnvironmentEnum.local, EnvironmentEnum.dev):
19 | root.router.include_router(dev.router)
20 |
21 | # Attach root router to app
22 | app.include_router(root.router)
23 |
--------------------------------------------------------------------------------
/docs/wiki/project/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Project Information
4 |
5 | - [Features](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/features.md)
6 | - [Releases](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/releases.md)
7 | - [Roadmap](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/roadmap.md)
8 | - [Milestones](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/milestones.md)
9 |
10 | ### Contributing
11 |
12 | If you're interested in participating in the project design discussions, or you want to actively submit work to the
13 | project then you should check out the
14 | [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md)!
15 |
--------------------------------------------------------------------------------
/src-api/routers/root.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from fastapi.responses import JSONResponse, RedirectResponse
3 | from lib.pda.api import NotFoundResponse, StatusResponse
4 |
5 | # Define generic responses to be used by routers
6 | router_responses: dict = {
7 | 404: {'model': NotFoundResponse},
8 | }
9 |
10 | router = APIRouter(
11 | prefix='',
12 | responses=router_responses,
13 | )
14 |
15 |
16 | @router.get('/', response_class=RedirectResponse)
17 | async def root() -> RedirectResponse:
18 | return RedirectResponse(url='/docs')
19 |
20 |
21 | @router.get('/status', response_model=StatusResponse)
22 | async def status() -> JSONResponse:
23 | response: StatusResponse = StatusResponse()
24 | return JSONResponse(response.model_dump(mode='json'))
25 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # lock-threads (https://github.com/marketplace/actions/lock-threads)
3 | name: 'Lock Inactive Threads'
4 |
5 | on:
6 | workflow_dispatch:
7 | schedule:
8 | - cron: '0 3 * * *'
9 |
10 | permissions:
11 | issues: write
12 | pull-requests: write
13 |
14 | jobs:
15 | lock:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: dessant/lock-threads@v3
19 | with:
20 | issue-inactive-days: 90
21 | pr-inactive-days: 30
22 | issue-lock-reason: 'resolved'
23 | exclude-any-issue-labels: 'bug / security-vulnerability, mod / announcement, mod / accepted, mod / reviewing, mod / testing'
24 | exclude-any-pr-labels: 'bug / security-vulnerability, mod / announcement, mod / accepted, mod / reviewing, mod / testing'
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Reference: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
3 | blank_issues_enabled: false
4 | contact_links:
5 | - name: 📖 Contribution Guide
6 | url: https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md
7 | about: "Please read through our contribution guide before opening an issue or pull request"
8 | - name: ❓ Discussion
9 | url: https://github.com/PowerDNS-Admin/pda-next/discussions
10 | about: "If you're just looking for help, try starting a discussion instead"
11 | - name: 💬 Discord Server
12 | url: https://discord.powerdnsadmin.org
13 | about: "Join our Discord server to discuss the project with other users and developers"
--------------------------------------------------------------------------------
/src-api/lib/config/db.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union
2 | from models.base import BaseConfig
3 |
4 |
5 | class DatabaseConnection(BaseConfig):
6 | host: str = 'localhost'
7 | port: int = 0
8 | username: Union[str, None] = None
9 | password: Union[str, None] = None
10 | database: Union[str, None] = None
11 |
12 |
13 | class MySQLDatabaseConnection(DatabaseConnection):
14 | port: int = 3306
15 |
16 |
17 | class RedisDatabaseConnection(DatabaseConnection):
18 | host: str = 'redis'
19 | port: int = 6379
20 | database: Union[int, str, None] = 0
21 |
22 |
23 | class DbConfig(BaseConfig):
24 | """A model that represents a configuration hierarchy for database connection settings."""
25 | mysql: MySQLDatabaseConnection
26 | redis: RedisDatabaseConnection
27 | sql_url: Optional[str] = 'sqlite+aiosqlite:///./pda.db'
28 |
--------------------------------------------------------------------------------
/src-api/templates/shared/mail/header.jinja2:
--------------------------------------------------------------------------------
1 | {% if settings.env in ['qa', 'dev', 'local'] %}
2 | {% if settings.env == 'qa' %}
3 | {% set mode = 'QA' %}
4 | {% elif settings.env == 'dev' %}
5 | {% set mode = 'Development' %}
6 | {% elif settings.env == 'local' %}
7 | {% set mode = 'Local' %}
8 | {% endif %}
9 |
10 |
11 |
12 | |
13 | *** {{ mode }} Version ***
14 | |
15 |
16 |
17 |
18 | {% endif %}
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/housekeeping.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🏡 Housekeeping
3 | description: A change pertaining to the codebase itself (developers only)
4 | labels: ["mod / change-request"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: >
9 | **NOTE:** This template is for use by maintainers only. Please do not submit
10 | an issue using this template unless you have been specifically asked to do so.
11 | - type: textarea
12 | attributes:
13 | label: Proposed Changes
14 | description: >
15 | Describe in detail the new feature or behavior you'd like to propose.
16 | Include any specific changes to work flows, data models, or the user interface.
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: Justification
22 | description: Please provide justification for the proposed change(s).
23 | validations:
24 | required: true
--------------------------------------------------------------------------------
/docs/wiki/configuration/settings/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Configuration Guide
4 |
5 | ### Application Settings Guide
6 |
7 | The application has two general categories of settings to be managed: environment configuration and
8 | runtime configuration. Environment configuration is used to configure the application during initialization.
9 | Runtime configuration is used to configure application features during runtime.
10 |
11 | #### Environment Configuration
12 |
13 | To view the alphabetical list of environment configuration settings, see the
14 | [Environment Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/settings/environment-settings.md).
15 |
16 | #### Runtime Configuration
17 |
18 | To view the alphabetical list of environment configuration settings, see the
19 | [Runtime Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/settings/runtime-settings.md).
20 |
--------------------------------------------------------------------------------
/deploy/docker/web/Dockerfile:
--------------------------------------------------------------------------------
1 | ########################################
2 | ########### PDA Web UI Image ###########
3 | ########################################
4 | FROM ubuntu:24.04
5 |
6 | ARG DEBIAN_FRONTEND=noninteractive
7 |
8 | # Install system dependencies via APT
9 | RUN apt update \
10 | && apt install -y build-essential pkg-config curl nginx \
11 | && rm -f /etc/nginx/sites-enabled/default
12 |
13 | # Install NodeJS
14 | RUN curl -fsSL https://deb.nodesource.com/setup_21.x | bash - \
15 | && apt install -y nodejs
16 |
17 | # Install NPM dependencies
18 | RUN npm -g install create-react-app@5.0.1 vite@5.4.14
19 |
20 | # Copy entrypoint script into image
21 | COPY ./deploy/docker/scripts/entrypoint-web.sh /srv/scripts/entrypoint.sh
22 |
23 | RUN chmod +x /srv/scripts/*
24 |
25 | # Change the working path of the execution context
26 | WORKDIR /srv/app
27 |
28 | # Copy the UI source files into the container
29 | COPY ./src-ui .
30 |
31 | ENTRYPOINT [ "/srv/scripts/entrypoint.sh" ]
32 |
33 | EXPOSE 8000
34 |
--------------------------------------------------------------------------------
/src-api/lib/config/__init__.py:
--------------------------------------------------------------------------------
1 | from lib.config.app import AppConfig
2 | from lib.config.api import ApiConfig
3 | from lib.config.celery import CeleryConfig
4 | from lib.config.db import DbConfig
5 | from lib.config.logging import LoggingConfig
6 | from lib.config.mail import MailConfig
7 | from lib.config.notifications import NotificationsConfig
8 | from lib.config.paths import PathsConfig
9 | from lib.config.server import ServerConfig
10 | from lib.config.services import ServicesConfig
11 | from lib.config.tasks import TasksConfig
12 | from models.base import BaseConfig
13 |
14 |
15 | class Config(BaseConfig):
16 | """A model that represents the root level element of the app configuration hierarchy."""
17 |
18 | api: ApiConfig
19 | app: AppConfig
20 | celery: CeleryConfig
21 | db: DbConfig
22 | logging: LoggingConfig
23 | mail: MailConfig
24 | notifications: NotificationsConfig
25 | paths: PathsConfig
26 | server: ServerConfig
27 | services: ServicesConfig
28 | tasks: TasksConfig
29 |
--------------------------------------------------------------------------------
/docs/wiki/support/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Project Support
4 |
5 | **Looking for help?** PDA has a somewhat active community of fellow users that may be able to provide assistance.
6 | Just [start a discussion](https://github.com/PowerDNS-Admin/pda-next/discussions/new) right here on GitHub!
7 |
8 | Looking to chat with someone? Join our [Discord Server](https://discord.powerdnsadmin.org).
9 |
10 | Some general tips for engaging here on GitHub:
11 |
12 | * Register for a free [GitHub account](https://github.com/signup) if you haven't already.
13 | * You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images.
14 | * To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.)
15 | * Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue.
16 |
--------------------------------------------------------------------------------
/src-api/models/api/auth.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from datetime import datetime
3 | from pydantic import ConfigDict
4 | from typing import Optional
5 | from models.base import BaseModel
6 |
7 |
8 | class UserSchema(BaseModel):
9 | """Represents an authentication user for API interactions."""
10 | model_config = ConfigDict(from_attributes=True)
11 |
12 | id: Optional[uuid.UUID] = None
13 | tenant_id: Optional[uuid.UUID] = None
14 | username: str
15 | status: str
16 | created_at: Optional[datetime] = None
17 | updated_at: Optional[datetime] = None
18 | authenticated_at: Optional[datetime] = None
19 |
20 |
21 | class ClientSchema(BaseModel):
22 | """Represents an authentication client for API interactions."""
23 | model_config = ConfigDict(from_attributes=True)
24 |
25 | id: Optional[uuid.UUID] = None
26 | tenant_id: Optional[uuid.UUID] = None
27 | user_id: Optional[uuid.UUID] = None
28 | name: str
29 | redirect_uri: str
30 | scopes: list[str]
31 | created_at: Optional[datetime] = None
32 | updated_at: Optional[datetime] = None
33 | expires_at: Optional[datetime] = None
34 |
--------------------------------------------------------------------------------
/docs/announcements/2023-03-10-docker-images-and-repository-branches.md:
--------------------------------------------------------------------------------
1 | As part of the clean-up process for the project, the project moving to a new development strategy based on the "[OneFlow](https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow)" methodology. As a result, I have created a "dev" branch which will now contain ongoing prerelease code base changes. This means that all PRs moving forward should be based on the "dev" branch and not the "master" branch.
2 |
3 | Following the next stable production release of version 0.4.0, the "master" branch will become tied to the current production release version. Accordingly, the "latest" tag of the project's Docker image (powerdnsadmin/pda-legacy) will continue to follow the "master" branch of the repository. This means that following the next production release of version 0.4.0, the "latest" Docker image tag will begin representing the latest production release.
4 |
5 | Additionally, the new "dev" branch of the repository has been setup to provide automatic Docker image builds under the "dev" tag of the official project Docker image powerdnsadmin/pda-legacy.
6 |
7 | Thank you all for participating in the community and/or being a loyal PDA user!
--------------------------------------------------------------------------------
/docs/announcements/2022-12-09-docker-hub-repository-moved.md:
--------------------------------------------------------------------------------
1 | Hi everyone,
2 |
3 | I wanted to share a brief but important announcement with the community. The project's associated Docker Hub repository has changed to a more permanent location to reflect the organization's branding and ongoing future efforts.
4 |
5 | The Docker Hub repository for the current PDA application is now located here: https://hub.docker.com/r/powerdnsadmin/pda-legacy
6 |
7 | This means that all legacy references to "ngoduykhanh/powerdns-admin" should be changed to "powerdnsadmin/pda-legacy" to continue receiving updates for images.
8 |
9 | It's important to take notice that all previous official releases **have NOT been been transferred** to this Docker Hub repository at the time of this post. I will continue working on a solution to get all of the previous releases built and published to the new repository as soon as possible. In the mean time, the best solution if you're using an image from a specific release, would be to continue using the old references until a further announcement has been made for this. The old repository should remain for quite some time until the transition is complete.
10 |
11 | Thank you all again for being loyal users of the PDA project!
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation_change.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 📖 Documentation Change
3 | description: Suggest an addition or modification to the PDA documentation
4 | labels: ["docs / request"]
5 | body:
6 | - type: dropdown
7 | attributes:
8 | label: Change Type
9 | description: What type of change are you proposing?
10 | options:
11 | - Addition
12 | - Correction
13 | - Removal
14 | - Cleanup (formatting, typos, etc.)
15 | validations:
16 | required: true
17 | - type: dropdown
18 | attributes:
19 | label: Area
20 | description: To what section of the documentation does this change primarily pertain?
21 | options:
22 | - Features
23 | - Installation/upgrade
24 | - Getting started
25 | - Configuration
26 | - Customization
27 | - Database Setup
28 | - Debug
29 | - Integrations/API
30 | - Administration
31 | - Development
32 | - Other
33 | validations:
34 | required: true
35 | - type: textarea
36 | attributes:
37 | label: Proposed Changes
38 | description: Describe the proposed changes and why they are necessary.
39 | validations:
40 | required: true
--------------------------------------------------------------------------------
/src-api/lib/config/mail.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from models.base import BaseConfig
3 |
4 |
5 | class MailConfig(BaseConfig):
6 | """A model that represents a configuration hierarchy for email features."""
7 |
8 | class MailServer(BaseConfig):
9 | class MailServerThrottle(BaseConfig):
10 | threshold: int = 5
11 | period: float = 30
12 | mode: str = 'sleep' # sleep or raise
13 | key: Optional[str] = None
14 | backoff_strategy: str = 'fixed' # fixed or exponential
15 | backoff_base: float = 1.0
16 | backoff_cap: float = 60.0
17 | jitter: bool = False
18 |
19 | alias: str
20 | host: str
21 | port: int = 25
22 | tls: bool = False
23 | ssl: bool = False
24 | local_hostname: Optional[str] = None
25 | source_address: Optional[tuple] = None
26 | timeout: Optional[int] = 60
27 | key_file: Optional[str] = None
28 | cert_file: Optional[str] = None
29 | username: Optional[str] = None
30 | password: Optional[str] = None
31 | from_address: Optional[str] = None
32 | throttle: MailServerThrottle
33 |
34 | servers: Optional[list[MailServer]] = None
35 |
--------------------------------------------------------------------------------
/deploy/docker/nginx/certs/proxy.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDOTCCAiGgAwIBAgIUblidLGvA7VS35JZgzf2cL9gLlcYwDQYJKoZIhvcNAQEL
3 | BQAwIjEgMB4GA1UEAwwXbG9jYWwucG93ZXJkbnNhZG1pbi5vcmcwHhcNMjUxMTI3
4 | MTgzMjI4WhcNMjUxMjI3MTgzMjI4WjAiMSAwHgYDVQQDDBdsb2NhbC5wb3dlcmRu
5 | c2FkbWluLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKt+L6W
6 | QQP/nby0tQInFqi319o0g6hqUawWRZSXcic6IcO7xlt6XgkM7n5GQlRbYjZAlmEd
7 | xfUUM6hjyJOqoDUuFr8k71UDU55MIO5HlU20YR6SM153YvBtq2AsIjknGrxPvcYJ
8 | vGaWcHV1tp79Cm3MszvPi6RM1f8V/ccKrbHGJjj10Mrc+KprJiY6lXPyQO4EBpvH
9 | Cn1nK3lj8pSBG5mk4h7t9Ed68wYUVACrmwJ1FZ9ePuzZIjLvmVggwZMSxzNYVNrV
10 | HFE85ysXAO7GO9G3/GKeLnR25qq8swjR74SxOCgMwIac1qaS/jCBSvnSVkJam8is
11 | Zw8x39hKM7SBZY0CAwEAAaNnMGUwIgYDVR0RBBswGYIXbG9jYWwucG93ZXJkbnNh
12 | ZG1pbi5vcmcwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1Ud
13 | DgQWBBTM49xR1OtcIVA3uRDz2mTQ/DdQwjANBgkqhkiG9w0BAQsFAAOCAQEAkYaH
14 | JtmfCMiFOvxYXoFkx/HrjJvJBBdMG5hhvHN+tmtPg7NDZyXF0Q5bSs/S6bKVnJTt
15 | qBT3FgcdlnlhYMHSZ5ZyIPgsRj6arHhMyyDqcENTL9GE9c7UmCf+XQhgIn44ZYgB
16 | J3TdhX13m82hABFRSznHYKb+fdwN3aWv+oxsxhr/YQKOO58EKKY8kTTdCwgTc2vQ
17 | yfZliBGRnCWaOZwPj4Nd3JyXcz1+5ZWil/qvo3mDjMkBSJzSW+eb5NiAJnU8YJ7b
18 | 6d2KPgaeYR5yKp8/Z/Q6hPvaIvmPUEi0poi9KOMmx0cGD3RhetTAnY1wOp6tI9hg
19 | 43h6aOOwhtapv0R7Bg==
20 | -----END CERTIFICATE-----
21 |
--------------------------------------------------------------------------------
/src-api/lib/security.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 | from enum import Enum
3 | from jose import jwt
4 | from passlib.context import CryptContext
5 |
6 | # TODO: Set the following constants from app settings
7 | COOKIE_NAME = 'session_id'
8 | ALGORITHM = "HS256"
9 | ACCESS_TOKEN_EXPIRE_MINUTES = 60
10 |
11 | crypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
12 |
13 |
14 | class TokenGrantTypeEnum(str, Enum):
15 | """Defines the supported OAuth token grant types."""
16 | client_credentials = 'client_credentials'
17 | password = 'password'
18 | refresh_token = 'refresh_token'
19 |
20 |
21 | def verify_hash(plain_value: str, hashed_value: str) -> bool:
22 | return crypt_context.verify(plain_value, hashed_value)
23 |
24 |
25 | def hash_value(value: str) -> str:
26 | return crypt_context.hash(value)
27 |
28 |
29 | def create_access_token(data: dict, expires_delta: timedelta | None = None):
30 | from datetime import timezone
31 | from app import config
32 |
33 | to_encode = data.copy()
34 | expire = datetime.now(tz=timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
35 | to_encode.update({"exp": expire})
36 |
37 | return jwt.encode(to_encode, config.app.secret_key, algorithm=ALGORITHM)
38 |
--------------------------------------------------------------------------------
/src-api/lib/services/twilio/messaging.py:
--------------------------------------------------------------------------------
1 | from pydantic import Field
2 | from typing import Optional
3 | from models.base import BaseModel
4 |
5 |
6 | class SMSMessage(BaseModel):
7 | """Provides a representation of a Twilio SMS message."""
8 |
9 | from_: Optional[str] = Field(
10 | description="The sender's phone number (in E.164 format).",
11 | alias="from",
12 | default=None,
13 | )
14 | """The sender's phone number (in E.164 format)."""
15 |
16 | to_: str = Field(
17 | description="The recipient's phone number J(in E.164 format) or channel address (e.g. whatsapp:+15552229999).",
18 | alias="to",
19 | )
20 | """The recipient's phone number J(in E.164 format) or channel address (e.g. whatsapp:+15552229999)."""
21 |
22 | body: str = Field(
23 | description="The text content of the message.",
24 | )
25 | """The text content of the message."""
26 |
27 |
28 | class MMSMessage(SMSMessage):
29 | """Provides a representation of a Twilio MMS message."""
30 |
31 | media_url: Optional[list[str]] = Field(
32 | description="The URL of media to include in the Message content. jpeg, jpg, gif, and png file types are fully supported by Twilio and content is formatted for delivery on destination devices.",
33 | alias="mediaUrl",
34 | )
35 |
--------------------------------------------------------------------------------
/docs/wiki/deployment/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## TL;DR
4 |
5 | To get started quickly with a simple deployment, execute the following commands on a *nix based system
6 | with `bash` and `git` installed:
7 |
8 | ```
9 | git clone https://github.com/PowerDNS-Admin/pda-next.git
10 | cd pda-next
11 | deployment/setup.sh
12 | ```
13 |
14 | ## Deployment Guides
15 |
16 | ### Bare Metal
17 |
18 | If you wish to deploy the application on bare metal, the project contains a plethora of guides for doing so.
19 |
20 | Please refer to the
21 | [Bare Metal Deployment Guides](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/README.md)
22 | for an index of currently available guides.
23 |
24 | ### Docker
25 |
26 | The project currently provides a few guides of common Docker deployment methods and templates.
27 |
28 | Please refer to the
29 | [Docker Deployment Guides](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/docker/README.md)
30 | for an index of currently available guides.
31 |
32 | ### Kubernetes
33 |
34 | The project currently provides a few guides of common Kubernetes deployment methods and templates.
35 |
36 | Please refer to the
37 | [Kubernetes Deployment Guides](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/kubernetes/README.md)
38 | for an index of currently available guides.
39 |
--------------------------------------------------------------------------------
/src-api/lib/pda/api/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from models.base import BaseModel
3 |
4 |
5 | class NotFoundResponse(BaseModel):
6 | """A response to a request for a resource that does not exist."""
7 | success: bool = False
8 | message: Optional[str] = 'The requested resource could not be found.'
9 |
10 | model_config = {
11 | 'json_schema_extra': {
12 | 'examples': [
13 | {
14 | 'success': False,
15 | 'message': 'The requested resource could not be found.',
16 | }
17 | ]
18 | }
19 | }
20 |
21 |
22 | class StatusResponse(BaseModel):
23 | """A response to a request for the system status."""
24 | status: str = 'ONLINE'
25 |
26 | model_config = {
27 | 'json_schema_extra': {
28 | 'examples': [
29 | {
30 | 'status': 'ONLINE',
31 | }
32 | ]
33 | }
34 | }
35 |
36 |
37 | class OperationResponse(BaseModel):
38 | """A generic response to an operation."""
39 | success: bool = True
40 | message: Optional[str] = None
41 |
42 | model_config = {
43 | 'json_schema_extra': {
44 | 'examples': [
45 | {
46 | 'success': True,
47 | 'message': 'Operation completed successfully',
48 | }
49 | ]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src-api/lib/config/api.py:
--------------------------------------------------------------------------------
1 | from models.base import BaseConfig
2 |
3 |
4 | class ApiConfig(BaseConfig):
5 | """A model that represents a configuration hierarchy for the FastAPI app."""
6 |
7 | class ApiRuntimeConfig(BaseConfig):
8 | class ApiRuntimeInitConfig(BaseConfig):
9 | repeat: bool = False
10 | repeat_interval: float = 300
11 | repeat_recovery_interval: float = 300
12 | init_db: bool = False
13 | repeat_db: bool = False
14 | repeat_db_interval: float = 300
15 | repeat_db_recovery_interval: float = 300
16 |
17 | init: ApiRuntimeInitConfig
18 |
19 | class ApiMetadataConfig(BaseConfig):
20 | class ApiMetadataTagConfig(BaseConfig):
21 | name: str
22 | description: str
23 |
24 | tags: list[ApiMetadataTagConfig] = []
25 |
26 | def __init__(self, **kwargs):
27 | super().__init__(**kwargs)
28 | if not self.tags:
29 | self.tags = [
30 | self.ApiMetadataTagConfig(name='default',
31 | description='Provides browser client entrypoint and monitoring functionality.'),
32 | self.ApiMetadataTagConfig(name='tasks',
33 | description='Provides task execution and monitoring features'),
34 | ]
35 |
36 | metadata: ApiMetadataConfig
37 | runtime: ApiRuntimeConfig
38 |
--------------------------------------------------------------------------------
/src-api/lib/config/app.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Optional
3 | from models.base import BaseConfig
4 |
5 |
6 | class EnvironmentEnum(str, Enum):
7 | """Defines the possible environment names."""
8 | prod = 'prod'
9 | qa = 'qa'
10 | dev = 'dev'
11 | local = 'local'
12 |
13 |
14 | class AppConfig(BaseConfig):
15 | """A model that represents a configuration hierarchy shared across descendant apps."""
16 |
17 | class AuthorConfig(BaseConfig):
18 | name: str = 'PowerDNS Admin'
19 | email: str = 'admin@powerdnsadmin.org'
20 | url: str = 'https://powerdnsadmin.org'
21 |
22 | class EnvironmentConfig(BaseConfig):
23 | class EnvironmentUrlsConfig(BaseConfig):
24 | api: str = None
25 | web: str = None
26 |
27 | name: EnvironmentEnum = EnvironmentEnum.prod
28 | prefix: str = 'PDA'
29 | file: str = 'config/.app.env'
30 | urls: EnvironmentUrlsConfig
31 |
32 | class MetadataConfig(BaseConfig):
33 | description: str = '''The PDA API provides a backend interface for core functionality and support of the UI.'''
34 |
35 | name: str = 'pda'
36 | version: str = '0.1.0'
37 | summary: str = 'A PowerDNS web interface with advanced management and automation features.'
38 | timezone: str = 'Etc/UTC'
39 | timezone_code: str = 'UTC'
40 | author: AuthorConfig
41 | environment: EnvironmentConfig
42 | metadata: MetadataConfig
43 | secret_key: Optional[str] = None
44 |
--------------------------------------------------------------------------------
/src-api/lib/config/services.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from models.base import BaseConfig
3 |
4 |
5 | class ServicesConfig(BaseConfig):
6 | """A model that represents a configuration hierarchy for external service integrations."""
7 |
8 | class TwilioConfig(BaseConfig):
9 | """Provides Twilio service configuration."""
10 |
11 | class TwilioApiConfig(BaseConfig):
12 | """Provides Twilio API configurations for the live and test environments."""
13 |
14 | class TwilioApiEnvironmentConfig(BaseConfig):
15 | """Provides Twilio API configuration for a specific environment."""
16 | account_sid: str
17 | auth_token: str
18 |
19 | live: TwilioApiEnvironmentConfig
20 | test: TwilioApiEnvironmentConfig
21 |
22 | class TwilioNumberConfig(BaseConfig):
23 | """Provides Twilio account number configuration."""
24 | sid: str
25 | number: str
26 | voice: bool = False
27 | sms: bool = False
28 | mms: bool = False
29 | faxes: bool = False
30 |
31 | api: TwilioApiConfig
32 | numbers: list[TwilioNumberConfig]
33 |
34 | class ZabbixConfig(BaseConfig):
35 | hostname: str = 'zabbix'
36 | port: int = 10051
37 | send_interval: float = 1
38 | default_node_name: str = 'pda'
39 | reporter_enabled: bool = True
40 | sender_enabled: bool = True
41 |
42 | twilio: Optional[TwilioConfig] = None
43 | zabbix: ZabbixConfig
44 |
--------------------------------------------------------------------------------
/src-api/lib/enums.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class TimeUnitsEnum(str, Enum):
5 | YEAR = 'year'
6 | MONTH = 'month'
7 | WEEK = 'week'
8 | DAY = 'day'
9 | HOUR = 'hour'
10 | MINUTE = 'minute'
11 | SECOND = 'second'
12 |
13 |
14 | class DayOfWeekEnum(str, Enum):
15 | SUNDAY = 'sunday'
16 | MONDAY = 'monday'
17 | TUESDAY = 'tuesday'
18 | WEDNESDAY = 'wednesday'
19 | THURSDAY = 'thursday'
20 | FRIDAY = 'friday'
21 | SATURDAY = 'saturday'
22 |
23 |
24 | class NotificationCategoryEnum(str, Enum):
25 | ALERT = 'alert'
26 | TASK_RECEIVED = 'task_received'
27 | TASK_REVOKED = 'task_revoked'
28 | TASK_REJECTED = 'task_rejected'
29 | TASK_PRE_RUN = 'task_pre_run'
30 | TASK_POST_RUN = 'task_post_run'
31 | TASK_SUCCESS = 'task_success'
32 | TASK_RETRY = 'task_retry'
33 | TASK_FAILED = 'task_failed'
34 | TASK_INTERNAL_ERROR = 'task_internal_error'
35 | TASK_UNKNOWN = 'task_unknown'
36 |
37 |
38 | class NotificationServiceEnum(str, Enum):
39 | MAIL = 'mail'
40 | MSTEAMS = 'msteams'
41 | TWILIO = 'twilio'
42 |
43 |
44 | class TaskEnum(str, Enum):
45 | PDA_ALERT = 'pda.alert'
46 | PDA_MAIL = 'pda.mail'
47 | PDA_MAIL_SEND = 'pda.mail.send'
48 | PDA_TEST = 'pda.test'
49 | PDA_TEST_MAIL = 'pda.test.mail'
50 | PDA_TEST_EXCEPTION = 'pda.test.exception'
51 | PDA_TEST_EXCEPTION_RETRY = 'pda.test.exception_retry'
52 | PDA_TEST_DELAY = 'pda.test.delay'
53 |
54 |
55 | class TwilioNotificationTypeEnum(str, Enum):
56 | VOICE = 'voice'
57 | MESSAGE = 'message'
58 |
--------------------------------------------------------------------------------
/src-api/worker.py:
--------------------------------------------------------------------------------
1 | import os
2 | from loguru import logger
3 | from celery import Celery
4 | from app import initialize
5 | from lib.celery import SignalHandler
6 |
7 | # Initialize the app with logging, environment settings, and file-based configuration
8 | config = initialize()
9 |
10 | # Initialize the Celery signal handler
11 | signal_handler = SignalHandler()
12 |
13 | # Instantiate the Celery application
14 | app = signal_handler.app = Celery(
15 | config.app.name,
16 | broker=config.celery.broker.url,
17 | backend=config.celery.backend.url,
18 | )
19 |
20 | # Set this app instance to be the default for all threads
21 | app.set_default()
22 |
23 | app.conf.beat_scheduler = 'src.lib.celery.DynamicScheduler'
24 | app.conf.timezone = 'UTC'
25 | app.conf.event_serializer = 'pickle'
26 | app.conf.task_serializer = 'pickle'
27 | app.conf.result_serializer = 'pickle'
28 | app.conf.result_extended = True
29 | app.conf.accept_content = ['json', 'application/json', 'application/x-python-serialize']
30 |
31 | # Set up task auto-discovery
32 | root_task_path = f'src/tasks'
33 | task_packages = []
34 |
35 | for dirpath, _, filenames in os.walk(root_task_path):
36 | for filename in filenames:
37 | if filename.endswith('.py') and filename != '__init__.py':
38 | full_path = os.path.join(dirpath, filename)
39 | rel_path = os.path.relpath(full_path, root_task_path)
40 | module = rel_path.replace(os.path.sep, '.').rsplit('.py', 1)[0]
41 | task_packages.append(f'tasks.{module}')
42 |
43 | logger.debug(f'Registering task packages for auto-discovery: {task_packages}')
44 |
45 | app.autodiscover_tasks(task_packages)
46 |
--------------------------------------------------------------------------------
/src-api/lib/prometheus.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 | from prometheus_fastapi_instrumentator.metrics import Info
3 |
4 | STATUS_MAP = {
5 | 'received': 1,
6 | 'running': 2,
7 | 'retry': 3,
8 | 'success': 4,
9 | 'failed': -1,
10 | 'revoked': -2,
11 | }
12 |
13 | STATUS_SQL = """
14 | WITH ranked_jobs AS (
15 | SELECT
16 | name,
17 | status,
18 | created_at,
19 | ROW_NUMBER() OVER (PARTITION BY name ORDER BY created_at DESC) AS rn
20 | FROM pda_task_jobs
21 | )
22 | SELECT name, status
23 | FROM ranked_jobs
24 | WHERE rn = 1;
25 | """
26 |
27 | def metric_last_task_status() -> Callable[[Info], None]:
28 | from prometheus_client.metrics import Gauge
29 | from sqlalchemy import text
30 | from sqlalchemy.orm import Session
31 | from app import config
32 | from lib.mysql import MysqlClient, MysqlDbConfig
33 |
34 | mysql_client = MysqlClient(MysqlDbConfig(**config.db.mysql.model_dump()), auto_connect=False)
35 |
36 | metric = Gauge(
37 | 'last_task_status',
38 | 'The last execution status of a task.',
39 | labelnames=('task',),
40 | )
41 |
42 | def instrumentation(info: Info) -> None:
43 | mysql_client.connect()
44 | session = Session(mysql_client.engine)
45 | result = session.execute(text(STATUS_SQL)).fetchall()
46 |
47 | for row in result:
48 | status_code = 0
49 |
50 | if row[1] in STATUS_MAP:
51 | status_code = STATUS_MAP[row[1]]
52 |
53 | metric.labels(task=row[0]).set(status_code)
54 |
55 | session.close()
56 | mysql_client.disconnect()
57 |
58 | return instrumentation
59 |
60 | def metric_setup(metrics):
61 | metrics.add(metric_last_task_status())
62 |
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/linux/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Linux Bare Metal Deployment Guides
4 |
5 | - [Alma Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/alma-linux.md)
6 | - [Alpine Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/alpine-linux.md)
7 | - [Arch Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/arch-linux.md)
8 | - [CentOS Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/centos-linux.md)
9 | - [Debian Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/debian-linux.md)
10 | - [Fedora Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/fedora-linux.md)
11 | - [FreeBSD Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/freebsd-linux.md)
12 | - [openSUSE Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/opensuse-linux.md)
13 | - [Oracle Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/oracle-linux.md)
14 | - [Redhat Enterprise Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/redhat-enterprise-linux.md)
15 | - [Rocky Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/rocky-linux.md)
16 | - [SUSE Linux Enterprise Server](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/suse-linux-enterprise-server.md)
17 | - [Ubuntu Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/ubuntu-linux.md)
18 |
--------------------------------------------------------------------------------
/deploy/docker/nginx/certs/proxy.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCyrfi+lkED/528
3 | tLUCJxaot9faNIOoalGsFkWUl3InOiHDu8Zbel4JDO5+RkJUW2I2QJZhHcX1FDOo
4 | Y8iTqqA1Lha/JO9VA1OeTCDuR5VNtGEekjNed2LwbatgLCI5Jxq8T73GCbxmlnB1
5 | dbae/QptzLM7z4ukTNX/Ff3HCq2xxiY49dDK3PiqayYmOpVz8kDuBAabxwp9Zyt5
6 | Y/KUgRuZpOIe7fRHevMGFFQAq5sCdRWfXj7s2SIy75lYIMGTEsczWFTa1RxRPOcr
7 | FwDuxjvRt/xini50duaqvLMI0e+EsTgoDMCGnNamkv4wgUr50lZCWpvIrGcPMd/Y
8 | SjO0gWWNAgMBAAECggEAIHIH26/UEYJOpdAYmFH5wtxTVVu41pGrz/hhXSidAXOE
9 | YxMLY1wWjEUtX3+plsDbVienRu5NnoGzGa4441uV6OI8Hix6SzEl83Y2ep5EBc6t
10 | 3gvFSS+bpzX66yVId6FasPw35SiqbeR+ek3MQhWH2cVUfJVk8YpER6Q6J3UQclnP
11 | olzkI4hjKmNevfxbZ0fl+hV7oCeR81R1lBOVB3mQzoRFY93vdOgyzf3+Lgae6bz1
12 | 0ss7ZhigB0WXbkDScjLyyooey4q3hf33RPchzRscuXBbLKvItW8g6OavVeyRaXcY
13 | UOUyMgegM9um+DUSUvoZadqir1qdrFNajfryDPqGAQKBgQDffeB3/zgJ/vT7Z4lT
14 | wzCJRhJuKqwVULkPPAVOE3YNMCATb19JvrO79FwyBSo1bXn0uAL9PlQL+eaGxoAo
15 | ZzeR7P0c3toxAKwsWwrSu/xwOXOk7UVhFrvRA2OEZtmp5NzmjILCPPaZI92vR5A3
16 | XH3W+kIpBZSHvrXLeqiPqiYpAQKBgQDMq27eJe2l+c5yvKWLvdRPCMTNuS0RfG/K
17 | +n9DR9zwneBn/GadeuZZR4vAGpKYwuaCHBHX/5jSNHuuucNCmNY6dZvSZw3sYsO0
18 | 88UW3SQrauWlCi0t12eMGLPGClt/iztVD3cgJyuC0lZAckLBgNxpFSIbMCwwhbb9
19 | 7r/U8CzQjQKBgBoqibJi7jO1ZwdcOubUroUT8Cp30of4WIJhG5nli2fF36uG2Zgv
20 | vKlf1b+BfUyeEa5GMQtnVb3FN2lGKlEQrJ/oKEZODSu5kW7sBdtgaRDWmSSRJxNT
21 | 7w9snyUsSYWrpvVTNCf7rT+GxHi2HztsF8uop0BYR+iQuoYlSUDwweEBAoGAEK4D
22 | +gzDQlyKa6VeJHZTACHp0A5AUwV+It+pUXVg1yc2q6LRRhJHBY2kIQLJYbO4j2/0
23 | MFM/RBpM3h97f0jvZJJDIbGOW+5snqmjLUrWcMdkcb/TkMHWSX+V3xTnAgz5x+Pb
24 | xH4MuLulldj6AcUbsWCsh+S0JTwwfp9feN4d+N0CgYA8PT7PKfHxVUX6b4ZnolJH
25 | aeoLESbVOMa13kC3qb22E6QG5s/eGffosjh/LniRWbnS6kNcrs5zSxGq/esUCuxM
26 | gupEPFWD0fxGxsXWirL8hE/sN4uLg+B1NvyxhrZ6Nu0aE7Pmo/dp6st0WqoE248i
27 | zVadQu8X64ySOdn2qlL8uw==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/docs/wiki/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Wiki Index
4 |
5 | ### Project Information
6 |
7 | - [Features](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/features.md)
8 | - [Releases](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/releases.md)
9 | - [Roadmap](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/roadmap.md)
10 | - [Milestones](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/milestones.md)
11 |
12 | For more information, see the [Project Information](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/README.md) section of the wiki.
13 |
14 | ### Support
15 |
16 | **Looking for help?** Please see the project's
17 | [Support Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/support/README.md)
18 | for more information.
19 |
20 | ### Contributing
21 |
22 | If you're interested in participating in the project design discussions, or you want to actively submit work to the
23 | project then you should check out the
24 | [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md)!
25 |
26 | ### Configuration
27 |
28 | For information on configuring the application, please visit the
29 | [Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/README.md).
30 |
31 | ### Deployment
32 |
33 | For information about how to deploy the application in various environments, please visit the
34 | [Deployment Guides](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/README.md).
35 |
36 | ### Testing
37 |
38 | For information on how to create and execute automated application tests, please visit the
39 | [Testing Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/testing/README.md).
40 |
--------------------------------------------------------------------------------
/src-api/lib/api/dependencies.py:
--------------------------------------------------------------------------------
1 | from fastapi import Depends, Request, HTTPException, status
2 | from fastapi.security import OAuth2PasswordBearer
3 | from sqlalchemy.ext.asyncio import AsyncSession
4 | from typing import AsyncGenerator
5 | from models.api.auth import UserSchema, ClientSchema
6 |
7 | oauth2_scheme_password = OAuth2PasswordBearer(tokenUrl='v1/token')
8 |
9 |
10 | async def validate_user_token_placeholder(token: str) -> UserSchema:
11 | pass
12 |
13 |
14 | async def validate_user_from_cookie(token: str) -> UserSchema:
15 | pass
16 |
17 |
18 | async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
19 | from app import AsyncSessionLocal
20 | async with AsyncSessionLocal() as session:
21 | yield session
22 |
23 |
24 | async def get_principal(request: Request, bearer_token: str = Depends(oauth2_scheme_password)) \
25 | -> UserSchema | ClientSchema:
26 | from loguru import logger
27 | from jose import JWTError, jwt
28 | from app import config
29 | from lib.security import ALGORITHM, COOKIE_NAME
30 |
31 | logger.warning(bearer_token)
32 |
33 | # Attempt OAuth Bearer Token Authentication
34 | if bearer_token:
35 | try:
36 | payload = jwt.decode(bearer_token, config.app.secret_key, algorithms=[ALGORITHM])
37 | logger.warning(payload)
38 | except JWTError:
39 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, 'Invalid bearer token')
40 | user = await validate_user_token_placeholder(bearer_token)
41 | if user:
42 | return user
43 |
44 | # Attempt Session Token Authentication
45 | cookie_token = request.cookies.get(COOKIE_NAME)
46 | if cookie_token:
47 | user = await validate_user_from_cookie(cookie_token)
48 | if user:
49 | return user
50 |
51 | # If neither works, raise an exception
52 | raise HTTPException(
53 | status_code=status.HTTP_401_UNAUTHORIZED,
54 | detail='Not authenticated'
55 | )
56 |
--------------------------------------------------------------------------------
/src-api/api.py:
--------------------------------------------------------------------------------
1 | import importlib, asyncio
2 | from contextlib import asynccontextmanager
3 | from fastapi import FastAPI
4 | from loguru import logger
5 | from prometheus_fastapi_instrumentator import Instrumentator
6 | from app import initialize, init_loop, init_db_loop
7 | from routers import install_routers
8 |
9 | # Initialize the app with logging, environment settings, and file-based configuration
10 | config = initialize()
11 |
12 | STARTUP_TASKS = [init_loop, init_db_loop]
13 | RUNNING_TASKS = []
14 |
15 |
16 | @asynccontextmanager
17 | async def lifespan(app: FastAPI):
18 | # Initialize all tasks defined in STARTUP_TASKS list
19 | for task in STARTUP_TASKS:
20 | RUNNING_TASKS.append(asyncio.create_task(task()))
21 |
22 | yield
23 |
24 | # Cancel all tasks defined in the RUNNING_TASKS list
25 | for task in RUNNING_TASKS:
26 | task.cancel()
27 |
28 |
29 | # Instantiate the FastAPI app
30 | app = FastAPI(
31 | lifespan=lifespan,
32 | title=config.app.name.title(),
33 | description=config.app.metadata.description,
34 | summary=config.app.summary,
35 | version=config.app.version,
36 | contact=config.app.author.model_dump(),
37 | openapi_tags=[t.model_dump() for t in config.api.metadata.tags],
38 | root_path=config.server.proxy_root,
39 | servers=[{'url': f'{config.app.environment.urls.api}/api/', 'description': 'PDA Environment API'}],
40 | )
41 |
42 | # FastAPI Middleware Configuration
43 | if config.server.middleware:
44 | for middleware in config.server.middleware:
45 | logger.debug(f'Loading FastAPI middleware: {middleware.name}')
46 | mw_parts = middleware.name.split('.')
47 | mw_mod = importlib.import_module('.'.join(mw_parts[:-1]))
48 | mw = getattr(mw_mod, mw_parts[-1])
49 | app.add_middleware(mw, **middleware.config if middleware.config else {})
50 |
51 | # Set up Prometheus metrics for the app
52 | metrics = Instrumentator()
53 | metrics.instrument(app).expose(app)
54 |
55 | # Set up FastAPI routers
56 | install_routers(app)
57 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Project Documentation
4 |
5 | Hopefully you will find that the project documentation is vast and answers most if not all of your questions. A key
6 | directive of the next-generation application is to reduce barrier to entry as much as possible within reason. To
7 | accomplish this goal, the project has provided great documentation surrounding the common topics of use such as
8 | application configuration, deployment, testing, and planning.
9 |
10 | ### Project Information
11 |
12 | For information about the project such as feature planning, the roadmap, and milestones, then please see the
13 | [Project Information](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/project/README.md) section of the wiki.
14 |
15 | ### Support
16 |
17 | **Looking for help?** Try taking a look at the project's
18 | [Support Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/support/README.md) or joining
19 | our [Discord Server](https://discord.powerdnsadmin.org).
20 |
21 | ### Contributing
22 |
23 | If you're interested in participating in the project design discussions, or you want to actively submit work to the
24 | project then you should check out the
25 | [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md)!
26 |
27 | ### Application Configuration
28 |
29 | For information about all the ways this application can be configured and what each setting does, please visit the
30 | [Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/README.md) section of the wiki.
31 |
32 | ### Application Deployment
33 |
34 | For information about how to deploy the application in various environments, please visit the
35 | [Deployment Guides](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/README.md) section of the wiki.
36 |
37 | ### Application Testing
38 |
39 | For information on how to create and execute automated application tests, please visit the
40 | [Testing Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/testing/README.md) section of the wiki.
41 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## No Warranty
4 |
5 | Per the terms of the MIT license, PDA is offered "as is" and without any guarantee or warranty pertaining to its operation. While every reasonable effort is made by its maintainers to ensure the product remains free of security vulnerabilities, users are ultimately responsible for conducting their own evaluations of each software release.
6 |
7 | ## Recommendations
8 |
9 | Administrators are encouraged to adhere to industry best practices concerning the secure operation of software, such as:
10 |
11 | * Do not expose your PDA installation to the public Internet
12 | * Do not permit multiple users to share an account
13 | * Enforce minimum password complexity requirements for local accounts
14 | * Prohibit access to your database from clients other than the PDA application
15 | * Keep your deployment updated to the most recent stable release
16 |
17 | ## Reporting a Suspected Vulnerability
18 |
19 | If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so via email. Please note that any reported vulnerabilities **MUST** meet all the following conditions:
20 |
21 | * Affects the most recent stable release of PDA, or a current beta release
22 | * Affects a PDA instance installed and configured per the official documentation
23 | * Is reproducible following a prescribed set of instructions
24 |
25 | Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
26 |
27 | If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/PowerDNS-Admin/pda-next/security/advisories/new) on GitHub, or email a brief description of the suspected bug and instructions for reproduction to **admin@powerdnsadmin.org**.
28 |
29 | ### Bug Bounties
30 |
31 | As PDA is provided as free open source software, we do not offer any monetary compensation for vulnerability or bug reports, however your contributions are greatly appreciated.
--------------------------------------------------------------------------------
/.github/workflows/project-release-test.yml:
--------------------------------------------------------------------------------
1 | name: 'Project Release (Test)'
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | test-build-publish-package:
8 | name: Test, Build, & Publish Python Package
9 | runs-on: ubuntu-20.04
10 | permissions:
11 | id-token: write
12 | contents: write
13 |
14 | steps:
15 | # Checkout the repository
16 | - name: Checkout Repository
17 | uses: actions/checkout@v4
18 |
19 | # Set up the environment to run Python
20 | - name: Set up Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: '3.8'
24 |
25 | # Set up the environment to run the latest Python build, test, and publish tools
26 | - name: Set Up Environment
27 | run: |
28 | python3 -m pip install --upgrade pip
29 | python3 -m pip install --upgrade setuptools
30 | python3 -m pip install --upgrade wheel
31 | python3 -m pip install --upgrade pytest
32 | python3 -m pip install --upgrade twine
33 |
34 | # Install the package from source
35 | - name: Install Source Code
36 | run: |
37 | python3 -m pip install .
38 |
39 | # Run the package tests
40 | - name: Test Source Code
41 | run: |
42 | python3 -m pytest -v
43 |
44 | # Build the package
45 | - name: Build Package
46 | run: |
47 | python3 -m pip install --upgrade build
48 | python3 -m build
49 |
50 | # Check the distribution files with Twine
51 | - name: Check Package
52 | run: |
53 | python3 -m twine check dist/*
54 |
55 | # Store the distribution files as artifacts
56 | - name: Upload Package
57 | uses: actions/upload-artifact@v3
58 | with:
59 | name: python-package
60 | path: dist/
61 |
62 | # Upload the distribution artifacts to PyPi test environment for all (supported) scenarios
63 | - name: Publish Package (PyPi Test)
64 | uses: pypa/gh-action-pypi-publish@release/v1
65 | with:
66 | packages-dir: dist/
67 | password: ${{ secrets.PYPI_TEST_TOKEN }}
68 | repository-url: https://test.pypi.org/legacy/
69 |
--------------------------------------------------------------------------------
/.github/workflows/image-build.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Image Build'
3 |
4 | on:
5 | workflow_dispatch:
6 | push:
7 | branches:
8 | - '-dev'
9 | - '-main'
10 | - '-dependabot/**'
11 | - '-feature/**'
12 | - '-issue/**'
13 | - '-release/**'
14 | tags:
15 | - '-v*.*.*'
16 | paths:
17 | - deployment/**
18 | - src/**
19 | - tests/**
20 | - setup.py
21 | pull_request:
22 | branches:
23 | - '-dev'
24 | - '-main'
25 | - '-dependabot/**'
26 | - '-feature/**'
27 | - '-issue/**'
28 | - '-release/**'
29 | paths:
30 | - deployment/**
31 | - src/**
32 | - tests/**
33 | - setup.py
34 |
35 | jobs:
36 | build-and-push-image:
37 | name: Build Image
38 | runs-on: ubuntu-latest
39 |
40 | steps:
41 | - name: Repository Checkout
42 | uses: actions/checkout@v2
43 |
44 | - name: Docker Image Metadata
45 | id: meta
46 | uses: docker/metadata-action@v3
47 | with:
48 | images: |
49 | powerdnsadmin/pda
50 | tags: |
51 | type=ref,event=tag
52 | type=semver,pattern={{version}}
53 | type=semver,pattern={{major}}.{{minor}}
54 | type=semver,pattern={{major}}
55 |
56 | - name: Docker Buildx Setup
57 | id: buildx
58 | uses: docker/setup-buildx-action@v1
59 |
60 | - name: Docker Hub Authentication
61 | uses: docker/login-action@v1
62 | with:
63 | username: ${{ secrets.DOCKERHUB_USERNAME_V2 }}
64 | password: ${{ secrets.DOCKERHUB_TOKEN_V2 }}
65 |
66 | - name: Docker Image Build
67 | uses: docker/build-push-action@v2
68 | with:
69 | context: ./
70 | file: ./docker/Dockerfile
71 | push: true
72 | tags: powerdnsadmin/pda:${{ github.ref_name }}
73 |
74 | - name: Docker Image Release Tagging
75 | uses: docker/build-push-action@v2
76 | if: ${{ startsWith(github.ref, 'refs/tags/v') }}
77 | with:
78 | context: ./
79 | file: ./deployment/docker/Dockerfile
80 | push: true
81 | tags: ${{ steps.meta.outputs.tags }}
82 | labels: ${{ steps.meta.outputs.labels }}
83 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
3 | name: 'Close Stale Threads'
4 |
5 | on:
6 | workflow_dispatch:
7 | schedule:
8 | - cron: '0 4 * * *'
9 |
10 | permissions:
11 | issues: write
12 | pull-requests: write
13 |
14 | jobs:
15 | stale:
16 |
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/stale@v6
20 | with:
21 | close-issue-message: >
22 | This issue has been automatically closed due to lack of activity. In an
23 | effort to reduce noise, please do not comment any further. Note that the
24 | core maintainers may elect to reopen this issue at a later date if deemed
25 | necessary.
26 | close-pr-message: >
27 | This PR has been automatically closed due to lack of activity.
28 | days-before-stale: 90
29 | days-before-close: 30
30 | exempt-issue-labels: 'bug / security-vulnerability, mod / announcement, mod / accepted, mod / reviewing, mod / testing'
31 | operations-per-run: 100
32 | remove-stale-when-updated: false
33 | stale-issue-label: 'mod / stale'
34 | stale-issue-message: >
35 | This issue has been automatically marked as stale because it has not had
36 | recent activity. It will be closed if no further activity occurs. PDA
37 | is governed by a small group of core maintainers which means not all opened
38 | issues may receive direct feedback. **Do not** attempt to circumvent this
39 | process by "bumping" the issue; doing so will result in its immediate closure
40 | and you may be barred from participating in any future discussions. Please see our
41 | [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md).
42 | stale-pr-label: 'mod / stale'
43 | stale-pr-message: >
44 | This PR has been automatically marked as stale because it has not had
45 | recent activity. It will be closed automatically if no further action is
46 | taken. Please see our
47 | [Contribution Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/contributing/README.md).
--------------------------------------------------------------------------------
/.github/workflows/python-build.yml:
--------------------------------------------------------------------------------
1 | name: 'Python Build'
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - '-dev'
8 | - '-main'
9 | - '-dependabot/**'
10 | - '-feature/**'
11 | - '-issue/**'
12 | - '-release/**'
13 | tags:
14 | - '-v*.*.*'
15 | paths:
16 | - src/**
17 | - tests/**
18 | - setup.py
19 | pull_request:
20 | branches:
21 | - '-dev'
22 | - '-main'
23 | - '-dependabot/**'
24 | - '-feature/**'
25 | - '-issue/**'
26 | - '-release/**'
27 | tags:
28 | - '-v*.*.*'
29 | paths:
30 | - src/**
31 | - tests/**
32 | - setup.py
33 |
34 | jobs:
35 | test-build-package:
36 | name: Test & Build Python Package
37 | runs-on: ubuntu-20.04
38 |
39 | steps:
40 | # Checkout the repository
41 | - name: Checkout Repository
42 | uses: actions/checkout@v4
43 |
44 | # Set up the environment to run Python
45 | - name: Set up Python
46 | uses: actions/setup-python@v5
47 | with:
48 | python-version: '3.8'
49 |
50 | # Set up the environment to run the latest Python build, test, and publish tools
51 | - name: Set Up Environment
52 | run: |
53 | python3 -m pip install --upgrade pip
54 | python3 -m pip install --upgrade setuptools
55 | python3 -m pip install --upgrade wheel
56 | python3 -m pip install --upgrade pytest
57 | python3 -m pip install --upgrade twine
58 |
59 | # Install the package from source
60 | - name: Install Source Code
61 | run: |
62 | python3 -m pip install .
63 |
64 | # Run the package tests
65 | - name: Test Source Code
66 | run: |
67 | python3 -m pytest -v
68 |
69 | # Build the package
70 | - name: Build Package
71 | run: |
72 | python3 -m pip install --upgrade build
73 | python3 -m build
74 |
75 | # Check the distribution files with Twine
76 | - name: Check Package
77 | run: |
78 | python3 -m twine check dist/*
79 |
80 | # Store the distribution files as artifacts
81 | - name: Upload Package
82 | uses: actions/upload-artifact@v3
83 | with:
84 | name: python-package
85 | path: dist/
86 |
--------------------------------------------------------------------------------
/docs/wiki/deployment/bare-metal/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Bare Metal Deployment Guides
4 |
5 | ### [BSD](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/bsd/README.md)
6 |
7 | - [FreeBSD](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/bsd/freebsd.md)
8 |
9 | ### [Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/README.md)
10 |
11 | - [Alma Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/alma-linux.md)
12 | - [Alpine Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/alpine-linux.md)
13 | - [Arch Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/arch-linux.md)
14 | - [CentOS Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/centos-linux.md)
15 | - [Debian Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/debian-linux.md)
16 | - [Fedora Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/fedora-linux.md)
17 | - [openSUSE Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/opensuse-linux.md)
18 | - [Oracle Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/oracle-linux.md)
19 | - [Redhat Enterprise Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/redhat-enterprise-linux.md)
20 | - [Rocky Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/rocky-linux.md)
21 | - [SUSE Linux Enterprise Server](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/suse-linux-enterprise-server.md)
22 | - [Ubuntu Linux](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/linux/ubuntu-linux.md)
23 |
24 | ### [Solaris](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/solaris/README.md)
25 |
26 | Coming soon!
27 |
28 | ### Other
29 |
30 | - [macOS](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/macos/README.md)
31 | - [Windows](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/deployment/bare-metal/windows/README.md)
32 |
--------------------------------------------------------------------------------
/src-api/models/base.py:
--------------------------------------------------------------------------------
1 | import types
2 | from pydantic import BaseModel as PydanticBaseModel
3 | from sqlalchemy import MetaData
4 | from sqlalchemy.orm import declarative_base
5 | from typing import List
6 |
7 | metadata = MetaData(
8 | naming_convention={
9 | "ix": "%(column_0_label)s",
10 | "uq": "%(table_name)s_%(column_0_name)s",
11 | "ck": "%(table_name)s_%(constraint_name)s",
12 | "fk": "%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
13 | "pk": "%(table_name)s",
14 | },
15 | )
16 |
17 | BaseSqlModel = declarative_base(metadata=metadata)
18 | """This provides an abstract base class for all SQL DB app models to inherit from."""
19 |
20 |
21 | class ValueHashMixIn:
22 |
23 | @property
24 | def _hash_keys(self) -> List[str]:
25 | return []
26 |
27 | @property
28 | def value_hash(self) -> str:
29 | """Generates a hash for this model's values based on the configured hash keys."""
30 | import hashlib, json
31 |
32 | if not self._hash_keys:
33 | raise ValueError('The model does not define any hash keys.')
34 |
35 | data = {
36 | key: getattr(self, key)
37 | for key in self._hash_keys
38 | if hasattr(self, key)
39 | }
40 |
41 | json_bytes = json.dumps(data, sort_keys=True, default=str).encode('utf-8')
42 |
43 | return hashlib.sha256(json_bytes).hexdigest()
44 |
45 | @staticmethod
46 | def drop_matches(queue, map1, map2, modify_in_place: bool = False) -> list[str]:
47 | final = queue.copy() if not modify_in_place else queue
48 | for name in queue.copy():
49 | if map1[name].value_hash == map2[name].value_hash:
50 | final.remove(name)
51 | return final
52 |
53 |
54 | class BaseModel(ValueHashMixIn, PydanticBaseModel):
55 | """This provides an abstract base class for all app models to inherit from."""
56 |
57 |
58 | class BaseConfig(PydanticBaseModel):
59 | """This provides an abstract base class for all configuration related models to inherit from."""
60 |
61 | def __init__(self, **kwargs):
62 | import inspect
63 |
64 | for key, field in self.__class__.model_fields.items():
65 | if key in kwargs:
66 | continue
67 |
68 | if inspect.isclass(field.annotation):
69 | if type(field.annotation) is types.GenericAlias:
70 | kwargs[key] = field.annotation()
71 |
72 | elif issubclass(field.annotation, BaseConfig):
73 | kwargs[key] = field.annotation()
74 |
75 | super().__init__(**kwargs)
76 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | description: Report a reproducible bug in the current release of PDA
4 | labels: ["bug / broken-feature"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: >
9 | **NOTE:** This form is only for reporting _reproducible bugs_ in a current PDA
10 | installation. If you're having trouble with installation or just looking for
11 | assistance with using PDA, please visit our
12 | [discussion forum](https://github.com/PowerDNS-Admin/pda-next/discussions) instead.
13 | - type: dropdown
14 | attributes:
15 | label: PDA version
16 | description: What version of PDA are you currently running?
17 | options:
18 | - "0.6.0"
19 | - "0.5.0"
20 | - "0.4.0"
21 | - "0.3.0"
22 | - "0.2.0"
23 | - "0.1.0"
24 | validations:
25 | required: true
26 | - type: dropdown
27 | attributes:
28 | label: Python version
29 | description: What version of Python are you currently running?
30 | options:
31 | - "3.0"
32 | - "3.1"
33 | - "3.2"
34 | - "3.3"
35 | - "3.4"
36 | - "3.5"
37 | - "3.6"
38 | - "3.7"
39 | - "3.8"
40 | - "3.9"
41 | - "3.10"
42 | - "3.11"
43 | validations:
44 | required: true
45 | - type: textarea
46 | attributes:
47 | label: Steps to Reproduce
48 | description: >
49 | Describe in detail the exact steps that someone else can take to
50 | reproduce this bug using the current stable release of PDA. Begin with the
51 | creation of any necessary database objects and call out every operation being
52 | performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
53 | the raw HTTP request(s) being made. Additionally, **do not rely on the demo instance** for reproducing
54 | suspected bugs, as its data is prone to modification or deletion at any time.
55 | placeholder: |
56 | 1. Click on "create widget"
57 | 2. Set foo to 12 and bar to G
58 | 3. Click the "create" button
59 | validations:
60 | required: true
61 | - type: textarea
62 | attributes:
63 | label: Expected Behavior
64 | description: What did you expect to happen?
65 | placeholder: A new zone record should have been created with the specified values
66 | validations:
67 | required: true
68 | - type: textarea
69 | attributes:
70 | label: Observed Behavior
71 | description: What happened instead?
72 | placeholder: A TypeError exception was raised
73 | validations:
74 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: ✨ Feature Request
3 | description: Propose a new PDA feature or enhancement
4 | labels: ["feature / request"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: >
9 | **NOTE:** This form is only for submitting well-formed proposals to extend or modify
10 | PDA in some way. If you're trying to solve a problem but can't figure out how, or if
11 | you still need time to work on the details of a proposed new feature, please start a
12 | [discussion](https://github.com/PowerDNS-Admin/pda-next/discussions) instead.
13 | - type: dropdown
14 | attributes:
15 | label: PDA version
16 | description: What version of PDA are you currently running?
17 | options:
18 | - "0.6.0"
19 | - "0.5.0"
20 | - "0.4.0"
21 | - "0.3.0"
22 | - "0.2.0"
23 | - "0.1.0"
24 | validations:
25 | required: true
26 | - type: dropdown
27 | attributes:
28 | label: Feature type
29 | options:
30 | - Data model modification
31 | - App Setting Addition
32 | - Default App Setting Change
33 | - New functionality
34 | - Change to existing functionality
35 | validations:
36 | required: true
37 | - type: textarea
38 | attributes:
39 | label: Proposed functionality
40 | description: >
41 | Describe in detail the new feature or behavior you are proposing. Include any specific changes
42 | to work flows, data models, and/or the user interface. The more detail you provide here, the
43 | greater chance your proposal has of being discussed. Feature requests which don't include an
44 | actionable implementation plan will be rejected.
45 | validations:
46 | required: true
47 | - type: textarea
48 | attributes:
49 | label: Use case
50 | description: >
51 | Explain how adding this functionality would benefit PDA users. What need does it address?
52 | validations:
53 | required: true
54 | - type: textarea
55 | attributes:
56 | label: Database changes
57 | description: >
58 | Note any changes to the database schema necessary to support the new feature. For example,
59 | does the proposal require adding a new model or field? (Not all new features require database
60 | changes.)
61 | - type: textarea
62 | attributes:
63 | label: External dependencies
64 | description: >
65 | List any new dependencies on external libraries or services that this new feature would
66 | introduce. For example, does the proposal require the installation of a new Python package?
67 | (Not all new features introduce new dependencies.)
--------------------------------------------------------------------------------
/src-api/routers/v1/auth.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Response, Depends, Form, HTTPException, status
2 | from sqlalchemy.ext.asyncio import AsyncSession
3 | from lib.api.dependencies import get_db_session
4 | from models.api.auth import UserSchema
5 | from routers.root import router_responses
6 |
7 | router = APIRouter(
8 | prefix='/auth',
9 | responses=router_responses,
10 | )
11 |
12 |
13 | @router.post('/login', response_model=UserSchema)
14 | async def login(
15 | response: Response,
16 | session:AsyncSession = Depends(get_db_session),
17 | username: str = Form(...),
18 | password: str = Form(...),
19 | ) -> UserSchema:
20 | from lib.security import COOKIE_NAME
21 | from models.db.auth import UserStatusEnum, User, Session
22 |
23 | # Delete any existing session cookie
24 | # FIXME: The following cookie delete isn't functioning
25 | response.delete_cookie(COOKIE_NAME, path='/')
26 |
27 | if not username or isinstance(username, str) and not len(username.strip()):
28 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, 'No username provided.')
29 |
30 | if not password or isinstance(password, str) and not len(password.strip()):
31 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, 'No password provided.')
32 |
33 | # TODO: Implement tenant segregation
34 |
35 | # Attempt to retrieve a user from the database based on the given username
36 | db_user = await User.get_by_username(session, username)
37 |
38 | if not db_user or not db_user.verify_password(password):
39 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, 'Invalid credentials provided.')
40 |
41 | # Ensure that the user has an appropriate status
42 | if db_user.status != UserStatusEnum.active:
43 | reason = 'This user is not active.'
44 |
45 | if db_user.status == UserStatusEnum.pending:
46 | reason = 'This user has not yet been invited.'
47 |
48 | if db_user.status == UserStatusEnum.invited:
49 | reason = 'This user has not yet been confirmed.'
50 |
51 | if db_user.status == UserStatusEnum.suspended:
52 | reason = 'This user has been suspended.'
53 |
54 | if db_user.status == UserStatusEnum.disabled:
55 | reason = 'This user has been disabled.'
56 |
57 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, reason)
58 |
59 | # Create the user schema from the database user
60 | user = UserSchema.model_validate(db_user)
61 |
62 | # TODO: Create a secure session cookie mechanism
63 |
64 | # Create a new auth session for the user
65 | auth_session = await Session.create_session(session, user)
66 |
67 | response.set_cookie(
68 | key=COOKIE_NAME,
69 | value=auth_session.id.hex,
70 | path='/',
71 | httponly=True,
72 | samesite='strict',
73 | secure=True,
74 | )
75 |
76 | return user
77 |
--------------------------------------------------------------------------------
/.github/workflows/project-release.yml:
--------------------------------------------------------------------------------
1 | name: 'Project Release'
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | test-build-publish-package:
8 | name: Test, Build, & Publish Python Package
9 | runs-on: ubuntu-20.04
10 | permissions:
11 | id-token: write
12 | contents: write
13 |
14 | steps:
15 | # Checkout the repository
16 | - name: Checkout Repository
17 | uses: actions/checkout@v4
18 |
19 | # Set up the environment to run Python
20 | - name: Set up Python
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: '3.8'
24 |
25 | # Set up the environment to run the latest Python build, test, and publish tools
26 | - name: Set Up Environment
27 | run: |
28 | python3 -m pip install --upgrade pip
29 | python3 -m pip install --upgrade setuptools
30 | python3 -m pip install --upgrade wheel
31 | python3 -m pip install --upgrade pytest
32 | python3 -m pip install --upgrade twine
33 |
34 | # Install the package from source
35 | - name: Install Source Code
36 | run: |
37 | python3 -m pip install .
38 |
39 | # Run the package tests
40 | - name: Test Source Code
41 | run: |
42 | python3 -m pytest -v
43 |
44 | # Build the package
45 | - name: Build Package
46 | run: |
47 | python3 -m pip install --upgrade build
48 | python3 -m build
49 |
50 | # Check the distribution files with Twine
51 | - name: Check Package
52 | run: |
53 | python3 -m twine check dist/*
54 |
55 | # Store the distribution files as artifacts
56 | - name: Upload Package
57 | uses: actions/upload-artifact@v3
58 | with:
59 | name: python-package
60 | path: dist/
61 |
62 | # Upload the distribution artifacts to PyPi production environment
63 | - name: Publish Package (PyPi)
64 | uses: pypa/gh-action-pypi-publish@release/v1
65 | with:
66 | packages-dir: dist/
67 | password: ${{ secrets.PYPI_TOKEN }}
68 |
69 | # Sign the distribution files with Sigstore
70 | - name: Sign the dists with Sigstore
71 | uses: sigstore/gh-action-sigstore-python@v1.2.3
72 | with:
73 | inputs: >-
74 | ./dist/*.tar.gz
75 | ./dist/*.whl
76 |
77 | # Create a GitHub Release
78 | - name: Create GitHub Release
79 | env:
80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81 | run: >-
82 | gh release create -d
83 | '${{ github.ref_name }}'
84 | --repo '${{ github.repository }}'
85 | --notes ""
86 |
87 | # Upload the distribution artifact signatures to the GitHub Release
88 | - name: Upload artifact signatures to GitHub Release
89 | env:
90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
91 | run: >-
92 | gh release upload
93 | '${{ github.ref_name }}' dist/**
94 | --repo '${{ github.repository }}'
95 |
--------------------------------------------------------------------------------
/docs/wiki/configuration/README.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Configuration Guide
4 |
5 | ### Getting Started
6 |
7 | The application provides a lot of fluidity in how it can be configured. This remains true for both environment
8 | and runtime configuration. The application is designed to be flexible and allow for a wide variety of
9 | deployment scenarios which include bare metal, virtual machines, containers, and cloud environments.
10 |
11 | There is a plethora of environment configuration settings that can be used to bootstrap the application for
12 | varying environments. All of these settings can be set using environment variables or by creating a
13 | `.env` file in the root directory of the application that contains one or more environment variables to be
14 | loaded at application startup. For more information on these settings, see the
15 | [Environment Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/settings/environment-settings.md)
16 | section.
17 |
18 | If you want to change the default path of the `.env` file, you can set the `PDA_ENV_FILE` environment variable
19 | to the path of the file you want to use. If a relative path is provided, it will be relative to the
20 | root directory of the application. Furthermore, if you want to use a different file encoding other than `UTF-8`,
21 | you may do so by setting the `PDA_ENV_FILE_ENCODING` environment variable to the encoding you want to use.
22 |
23 | #### Secrets Support
24 |
25 | Additionally, there is support for secure settings to be kept in a filesystem location using a convention
26 | similar to Docker-style secrets. To use this feature, you simply create a file with the same name as the
27 | application setting you want to set and store it in the directory specified by the `env_secrets_dir` setting
28 | or the `PDA_ENV_SECRETS_DIR` environment variable.
29 |
30 | So for example, say you want to set the value of an application setting named `example_option`.
31 | Assuming that `env_secrets_dir` or `PDA_ENV_SECRETS_DIR` is set to `/var/run/secrets`, one would create a file
32 | named `example_option` and store it in the `/run/secrets` directory. The contents of the file would be
33 | the value of the `example_option` setting. The application will automatically detect the file and use its
34 | contents as the value of the setting.
35 |
36 | ### Application Settings
37 |
38 | To get an in-depth understanding of the many application settings available, see the
39 | [Application Settings Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/settings/README.md).
40 |
41 | #### Environment Configuration
42 |
43 | To view the alphabetical list of environment configuration settings, see the
44 | [Environment Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/settings/environment-settings.md).
45 |
46 | #### Runtime Configuration
47 |
48 | To view the alphabetical list of environment configuration settings, see the
49 | [Runtime Configuration Guide](https://github.com/PowerDNS-Admin/pda-next/blob/main/docs/wiki/configuration/settings/runtime-settings.md).
50 |
--------------------------------------------------------------------------------
/src-api/lib/services/microsoft/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 |
4 | class MicrosoftTeams:
5 | """This class provides an API for interacting with the Microsoft Teams API."""
6 |
7 | STYLE_ATTENTION = 'attention'
8 | STYLE_GOOD = 'good'
9 | STYLE_EMPHASIS = 'emphasis'
10 |
11 | @staticmethod
12 | def send_alert(webhook_url: str, title: str, message: str, style: Optional[str] = None, url: Optional[str] = None,
13 | url_text: Optional[str] = None):
14 | """This method is used to send an alert to Microsoft Teams via webhook."""
15 | import json
16 | import requests
17 | from loguru import logger
18 |
19 | headers = {'Content-Type': 'application/json'}
20 |
21 | payload = {
22 | "type": "message",
23 | "attachments": [{
24 | "contentType": "application/vnd.microsoft.card.adaptive",
25 | "content": {
26 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
27 | "type": "AdaptiveCard",
28 | "version": "1.2",
29 | "body": [
30 | {
31 | "type": "Container",
32 | "items": [
33 | {
34 | "type": "TextBlock",
35 | "size": "Medium",
36 | "weight": "Bolder",
37 | "wrap": True,
38 | "text": title,
39 | },
40 | ],
41 | "bleed": True,
42 | }
43 | ],
44 | }
45 | }]
46 | }
47 |
48 | messages = message.split('\n')
49 |
50 | for msg in messages:
51 | if not len(msg.strip()):
52 | continue
53 | payload['attachments'][0]['content']['body'].append({
54 | "type": "TextBlock",
55 | "text": msg,
56 | "wrap": True,
57 | })
58 |
59 | if isinstance(style, str):
60 | payload['attachments'][0]['content']['body'][0]['style'] = style
61 |
62 | if isinstance(url, str):
63 | payload['attachments'][0]['content']['actions'] = [
64 | {
65 | "type": "Action.OpenUrl",
66 | "title": url_text if isinstance(url_text, str) else 'Open',
67 | "url": url,
68 | }
69 | ]
70 |
71 | response = requests.post(webhook_url, headers=headers, data=json.dumps(payload))
72 |
73 | if 200 <= response.status_code < 300:
74 | logger.debug(f'MicrosoftTeams: Successfully sent alert to Microsoft Teams webhook.')
75 | else:
76 | logger.error(f'MicrosoftTeams: Failed to send alert to Microsoft Teams webhook:'
77 | + f'\nStatus Code: {response.status_code}\nResponse: {response.text}')
78 |
--------------------------------------------------------------------------------
/src-api/models/db/system.py:
--------------------------------------------------------------------------------
1 | """
2 | PDA System Database Models
3 |
4 | This file defines the database models associated with core system functionality.
5 | """
6 | import uuid
7 | from datetime import datetime
8 | from sqlalchemy import DateTime, Integer, String, TEXT, Uuid, text
9 | from sqlalchemy.orm import Mapped, mapped_column, relationship
10 | from models.base import BaseSqlModel
11 |
12 |
13 | class StopgapDomain(BaseSqlModel):
14 | """Represents a stopgap domain."""
15 |
16 | __tablename__ = 'pda_stopgap_domains'
17 | """Defines the database table name."""
18 |
19 | id: Mapped[str] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
20 | """The unique identifier of the record."""
21 |
22 | name: Mapped[str] = mapped_column(String(100), nullable=False)
23 | """The friendly name of the stopgap domain."""
24 |
25 | fqdn: Mapped[str] = mapped_column(String(253), nullable=False)
26 | """The FQDN for the base stopgap domain."""
27 |
28 | restricted_hosts: Mapped[list[str]] = mapped_column(TEXT, nullable=True)
29 | """The list of hostnames that are restricted from use by tenants."""
30 |
31 | created_at: Mapped[datetime] = mapped_column(
32 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
33 | )
34 | """The timestamp representing when the record was created."""
35 |
36 | updated_at: Mapped[datetime] = mapped_column(
37 | DateTime, nullable=False, default=datetime.now, onupdate=datetime.now,
38 | server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP')
39 | )
40 | """The timestamp representing when the record was last updated."""
41 |
42 | tenants = relationship('Tenant', back_populates='stopgap_domain')
43 | """A list of tenants associated with the record."""
44 |
45 |
46 | class RefTimezone(BaseSqlModel):
47 | """Represents an IANA timezone."""
48 |
49 | __tablename__ = 'pda_ref_timezones'
50 | """Defines the database table name."""
51 |
52 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
53 | """The unique identifier of the record."""
54 |
55 | name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
56 | """The unique IANA name for the timezone."""
57 |
58 | offset: Mapped[int] = mapped_column(Integer, nullable=False)
59 | """The offset from UTC in seconds for the timezone."""
60 |
61 | offset_dst: Mapped[int] = mapped_column(Integer, nullable=False)
62 | """The offset from UTC in seconds during daylight savings time for the timezone."""
63 |
64 | created_at: Mapped[datetime] = mapped_column(
65 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
66 | )
67 | """The timestamp representing when the record was created."""
68 |
69 | updated_at: Mapped[datetime] = mapped_column(
70 | DateTime, nullable=False, default=datetime.now, onupdate=datetime.now,
71 | server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP')
72 | )
73 | """The timestamp representing when the record was last updated."""
74 |
--------------------------------------------------------------------------------
/src-api/routers/v1/__init__.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Form, Depends, HTTPException
2 | from sqlalchemy.ext.asyncio import AsyncSession
3 | from lib.api.dependencies import get_db_session
4 | from routers.root import router_responses
5 | from routers.v1 import auth, services, tasks
6 |
7 | router = APIRouter(
8 | prefix='/v1',
9 | responses=router_responses,
10 | )
11 |
12 | # Setup descendent routers
13 | router.include_router(auth.router)
14 | router.include_router(services.router)
15 | router.include_router(tasks.router)
16 |
17 |
18 | @router.post('/token')
19 | async def token(
20 | grant_type: str = Form(...),
21 | client_id: str = Form(...),
22 | client_secret: str = Form(...),
23 | username: str = Form(None),
24 | password: str = Form(None),
25 | refresh_token: str = Form(None),
26 | session: AsyncSession = Depends(get_db_session),
27 | ):
28 | """Handle OAuth token grants."""
29 | from lib.security import create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, TokenGrantTypeEnum
30 | from models.db.auth import Client, RefreshToken, User
31 |
32 | # Validate Client
33 | client = await Client.get_by_id(session, client_id)
34 | if not client or not client.verify_secret(client_secret):
35 | raise HTTPException(400, 'invalid_client')
36 |
37 | # Client Credential Grants
38 | if grant_type == TokenGrantTypeEnum.client_credentials.value:
39 | access = create_access_token({'sub': f'client:{client_id}'})
40 | refresh = await RefreshToken.create_token(session, client_id)
41 |
42 | return {
43 | 'access_token': access,
44 | 'token_type': 'bearer',
45 | 'expires_in': ACCESS_TOKEN_EXPIRE_MINUTES,
46 | 'refresh_token': str(refresh.id),
47 | }
48 |
49 | # Username Credential Grants
50 | if grant_type == TokenGrantTypeEnum.password.value:
51 | user = await User.get_by_username(session, username)
52 | if not user or not user.verify_password(password):
53 | raise HTTPException(400, 'invalid_user')
54 |
55 | access = create_access_token({'sub': str(user.id)})
56 | refresh = await RefreshToken.create_token(session, client_id, str(user.id))
57 |
58 | return {
59 | 'access_token': access,
60 | 'token_type': 'bearer',
61 | 'expires_in': ACCESS_TOKEN_EXPIRE_MINUTES,
62 | 'refresh_token': str(refresh.id),
63 | }
64 |
65 | # Refresh Token Grants
66 | if grant_type == TokenGrantTypeEnum.refresh_token.value:
67 | stored = await RefreshToken.get_by_id(session, refresh_token)
68 | if not stored or stored.revoked or stored.client_id != client_id:
69 | raise HTTPException(400, 'invalid_grant')
70 |
71 | await RefreshToken.revoke_token(session, stored)
72 |
73 | access = create_access_token({'sub': str(stored.user_id) if stored.user_id else f'client:{client_id}'})
74 | refresh = await RefreshToken.create_token(session, client_id, str(stored.user_id))
75 |
76 | return {
77 | 'access_token': access,
78 | 'token_type': 'bearer',
79 | 'expires_in': ACCESS_TOKEN_EXPIRE_MINUTES,
80 | 'refresh_token': str(refresh.id),
81 | }
82 |
83 | raise HTTPException(400, 'unsupported_grant_type')
84 |
--------------------------------------------------------------------------------
/deploy/docker/nginx/conf/proxy.conf.template:
--------------------------------------------------------------------------------
1 |
2 | upstream backend_api {
3 | server pda_api:8000 fail_timeout=10 max_fails=1;
4 | }
5 |
6 | upstream backend_ws {
7 | server pda_api:8001 fail_timeout=10 max_fails=1;
8 | }
9 |
10 | upstream backend_web {
11 | server pda_web:8000 fail_timeout=10 max_fails=1;
12 | }
13 |
14 | server {
15 | listen 80;
16 | server_name _;
17 | access_log /var/log/nginx/proxy.access.log combined;
18 | error_log /var/log/nginx/proxy.error.log error;
19 | return 301 https://$host$request_uri;
20 | }
21 |
22 | server {
23 | listen 443 ssl;
24 | server_name proxy 127.0.0.1 ${PDA_HOSTNAME};
25 | access_log /var/log/nginx/proxy.access.log combined;
26 | error_log /var/log/nginx/proxy.error.log error;
27 |
28 | ssl_certificate /etc/nginx/certs/proxy.crt;
29 | ssl_certificate_key /etc/nginx/certs/proxy.key;
30 | ssl_protocols TLSv1.2 TLSv1.3;
31 | ssl_ciphers HIGH:!aNULL:!MD5;
32 |
33 | root /var/www/html;
34 |
35 | location / {
36 | proxy_pass http://backend_web;
37 | proxy_pass_request_headers on;
38 | proxy_set_header Host $host;
39 | proxy_set_header X-Real-IP $remote_addr;
40 | proxy_set_header X-Forwarded-For $remote_addr;
41 | proxy_connect_timeout 5s;
42 | proxy_send_timeout 5s;
43 | proxy_read_timeout 5s;
44 | }
45 |
46 | location /openapi.json {
47 | client_max_body_size 0;
48 | proxy_pass http://backend_api/openapi.json;
49 | proxy_pass_request_headers on;
50 | proxy_set_header Host $host;
51 | proxy_set_header X-Real-IP $remote_addr;
52 | proxy_set_header X-Forwarded-For $remote_addr;
53 | proxy_set_header X-Forwarded-Proto $scheme;
54 | proxy_set_header X-Forwarded-Host $host;
55 | proxy_set_header X-Forwarded-Prefix /;
56 | proxy_connect_timeout 15s;
57 | proxy_send_timeout 120s;
58 | proxy_read_timeout 120s;
59 | }
60 |
61 | location /api/ {
62 | client_max_body_size 0;
63 | proxy_pass http://backend_api/;
64 | proxy_pass_request_headers on;
65 | proxy_set_header Host $host;
66 | proxy_set_header X-Real-IP $remote_addr;
67 | proxy_set_header X-Forwarded-For $remote_addr;
68 | proxy_set_header X-Forwarded-Proto $scheme;
69 | proxy_set_header X-Forwarded-Host $host;
70 | proxy_set_header X-Forwarded-Prefix /api;
71 | proxy_connect_timeout 15s;
72 | proxy_send_timeout 120s;
73 | proxy_read_timeout 120s;
74 | }
75 |
76 | location /ws {
77 | proxy_pass http://backend_ws;
78 | proxy_pass_request_headers on;
79 | proxy_set_header Host $host;
80 | proxy_set_header X-Real-IP $remote_addr;
81 | proxy_set_header X-Forwarded-For $remote_addr;
82 | proxy_set_header Upgrade $http_upgrade;
83 | proxy_set_header Connection "upgrade";
84 | proxy_http_version 1.1;
85 | }
86 |
87 | location /hmr {
88 | proxy_pass http://backend_web;
89 | proxy_pass_request_headers on;
90 | proxy_set_header Host $host;
91 | proxy_set_header X-Real-IP $remote_addr;
92 | proxy_set_header X-Forwarded-For $remote_addr;
93 | proxy_set_header Upgrade $http_upgrade;
94 | proxy_set_header Connection "upgrade";
95 | proxy_http_version 1.1;
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [admin@powerdnsadmin.org](mailto:admin@powerdnsadmin.org). All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | labels:
2 | - name: bug / broken-feature
3 | description: Existing feature malfunctioning or broken
4 | color: 'd73a4a'
5 | - name: bug / security-vulnerability
6 | description: Security vulnerability identified with the application
7 | color: 'd73a4a'
8 | - name: docs / discussion
9 | description: Documentation change proposals
10 | color: '0075ca'
11 | - name: docs / request
12 | description: Documentation change request
13 | color: '0075ca'
14 | - name: feature / dependency
15 | description: Existing feature dependency
16 | color: '008672'
17 | - name: feature / discussion
18 | description: New or existing feature discussion
19 | color: '008672'
20 | - name: feature / request
21 | description: New feature or enhancement request
22 | color: '008672'
23 | - name: feature / update
24 | description: Existing feature modification
25 | color: '008672'
26 | - name: help / deployment
27 | description: Questions regarding application deployment
28 | color: 'd876e3'
29 | - name: help / features
30 | description: Questions regarding the use of application features
31 | color: 'd876e3'
32 | - name: help / other
33 | description: General questions not specific to application deployment or features
34 | color: 'd876e3'
35 | - name: mod / accepted
36 | description: This request has been accepted
37 | color: 'e5ef23'
38 | - name: mod / announcement
39 | description: This is an admin announcement
40 | color: 'e5ef23'
41 | - name: mod / change-request
42 | description: Used by internal developers to indicate a change-request.
43 | color: 'e5ef23'
44 | - name: mod / changes-requested
45 | description: Changes have been requested before proceeding
46 | color: 'e5ef23'
47 | - name: mod / duplicate
48 | description: This issue or pull request already exists
49 | color: 'e5ef23'
50 | - name: mod / good-first-issue
51 | description: Good for newcomers
52 | color: 'e5ef23'
53 | - name: mod / help-wanted
54 | description: Extra attention is needed
55 | color: 'e5ef23'
56 | - name: mod / invalid
57 | description: This doesn't seem right
58 | color: 'e5ef23'
59 | - name: mod / rejected
60 | description: This request has been rejected
61 | color: 'e5ef23'
62 | - name: mod / reviewed
63 | description: This request has been reviewed
64 | color: 'e5ef23'
65 | - name: mod / reviewing
66 | description: This request is being reviewed
67 | color: 'e5ef23'
68 | - name: mod / stale
69 | description: This request has gone stale
70 | color: 'e5ef23'
71 | - name: mod / tested
72 | description: This has been tested
73 | color: 'e5ef23'
74 | - name: mod / testing
75 | description: This is being tested
76 | color: 'e5ef23'
77 | - name: mod / wont-fix
78 | description: This will not be worked on
79 | color: 'e5ef23'
80 | - name: skill / database
81 | description: Requires a database skill-set
82 | color: '5319E7'
83 | - name: skill / docker
84 | description: Requires a Docker skill-set
85 | color: '5319E7'
86 | - name: skill / documentation
87 | description: Requires a documentation skill-set
88 | color: '5319E7'
89 | - name: skill / html
90 | description: Requires a HTML skill-set
91 | color: '5319E7'
92 | - name: skill / javascript
93 | description: Requires a JavaScript skill-set
94 | color: '5319E7'
95 | - name: skill / python
96 | description: Requires a Python skill-set
97 | color: '5319E7'
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | services:
2 |
3 | redis:
4 | image: redis:8.0.1
5 | container_name: pda_redis
6 | hostname: redis
7 | restart: unless-stopped
8 | env_file: .env
9 | volumes:
10 | - redis:/data
11 | networks:
12 | - pda
13 | healthcheck:
14 | test: [ 'CMD', 'redis-cli', '--raw', 'incr', 'ping' ]
15 | interval: 30s
16 | timeout: 10s
17 | retries: 3
18 | start_period: 15s
19 | start_interval: 5s
20 |
21 | proxy:
22 | image: nginx:1.27
23 | container_name: pda_proxy
24 | hostname: pda_proxy
25 | restart: unless-stopped
26 | env_file: .env
27 | volumes:
28 | - ./deploy/docker/nginx/certs:/etc/nginx/certs
29 | - ./deploy/docker/nginx/conf/proxy.conf.template:/etc/nginx/templates/default.conf.template
30 | - ./var/log/nginx:/var/log/nginx
31 | ports:
32 | - "${PDA_SERVICE_IP}:80:80"
33 | - "${PDA_SERVICE_IP}:443:443"
34 | networks:
35 | - pda
36 | healthcheck:
37 | test: [ 'CMD', 'curl', '-f', 'http://pda_proxy:80' ]
38 | interval: 30s
39 | timeout: 10s
40 | retries: 3
41 | start_period: 15s
42 | start_interval: 5s
43 |
44 | api:
45 | image: powerdnsadmin/pda-api:latest
46 | container_name: pda_api
47 | hostname: pda_api
48 | restart: unless-stopped
49 | command: python3 -m uvicorn --host 0.0.0.0 --port 8081 --root-path / --log-level info --access-log --proxy-headers api:app
50 | build:
51 | context: .
52 | dockerfile: ./deploy/docker/api/Dockerfile
53 | depends_on:
54 | - redis
55 | env_file: .env
56 | volumes:
57 | - ./config:/srv/app/config
58 | networks:
59 | - pda
60 | healthcheck:
61 | test: [ 'CMD', 'curl', '-f', 'http://pda_api:8081/status' ]
62 | interval: 30s
63 | timeout: 10s
64 | retries: 3
65 | start_period: 15s
66 | start_interval: 5s
67 |
68 | worker:
69 | image: powerdnsadmin/pda-api:latest
70 | container_name: pda_worker
71 | hostname: pda_worker
72 | restart: unless-stopped
73 | command: python3 -m celery -A worker.app worker --beat --loglevel debug
74 | build:
75 | context: .
76 | dockerfile: ./deploy/docker/api/Dockerfile
77 | depends_on:
78 | - redis
79 | env_file: .env
80 | volumes:
81 | - ./config:/srv/app/config
82 | networks:
83 | - pda
84 | healthcheck:
85 | test: [ 'CMD-SHELL', 'celery', '-A', 'worker.app', 'inspect', 'ping', '--destination', 'celery@$$HOSTNAME' ]
86 | interval: 30s
87 | timeout: 10s
88 | retries: 3
89 | start_period: 15s
90 | start_interval: 5s
91 | stop_signal: SIGTERM
92 | stop_grace_period: 900s
93 |
94 | web:
95 | image: powerdnsadmin/pda-web:latest
96 | restart: unless-stopped
97 | container_name: pda_web
98 | hostname: pda_web
99 | build:
100 | context: ./
101 | dockerfile: ./deploy/docker/web/Dockerfile
102 | env_file: .env
103 | volumes:
104 | - ./src-ui:/srv/app
105 | - ./deploy/docker/nginx/conf/web.conf:/etc/nginx/sites-enabled/default.conf
106 | - ./var/log/nginx:/var/log/nginx
107 | networks:
108 | - pda
109 | healthcheck:
110 | test: [ 'CMD', 'curl', '-f', 'http://pda_web:8000' ]
111 | interval: 30s
112 | timeout: 10s
113 | retries: 3
114 | start_period: 15s
115 | start_interval: 5s
116 |
117 | volumes:
118 | redis:
119 |
120 | networks:
121 | pda:
122 | external: false
123 | name: pda
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | services:
2 |
3 | redis:
4 | image: redis:8.0.1
5 | container_name: pda_redis
6 | hostname: redis
7 | restart: unless-stopped
8 | env_file: .env
9 | volumes:
10 | - redis:/data
11 | networks:
12 | - pda
13 | healthcheck:
14 | test: [ 'CMD', 'redis-cli', '--raw', 'incr', 'ping' ]
15 | interval: 30s
16 | timeout: 10s
17 | retries: 3
18 | start_period: 15s
19 | start_interval: 5s
20 |
21 | proxy:
22 | image: nginx:1.27
23 | container_name: pda_proxy
24 | hostname: pda_proxy
25 | restart: unless-stopped
26 | env_file: .env
27 | volumes:
28 | - ./deploy/docker/nginx/certs:/etc/nginx/certs
29 | - ./deploy/docker/nginx/conf/proxy.conf.template:/etc/nginx/templates/default.conf.template
30 | - ./var/log/nginx:/var/log/nginx
31 | ports:
32 | - "${PDA_SERVICE_IP}:80:80"
33 | - "${PDA_SERVICE_IP}:443:443"
34 | networks:
35 | - pda
36 | healthcheck:
37 | test: [ 'CMD', 'curl', '-f', 'http://pda_proxy:80' ]
38 | interval: 30s
39 | timeout: 10s
40 | retries: 3
41 | start_period: 15s
42 | start_interval: 5s
43 |
44 | api:
45 | image: powerdnsadmin/pda-api:latest
46 | container_name: pda_api
47 | hostname: pda_api
48 | restart: unless-stopped
49 | command: python3 -m uvicorn --host 0.0.0.0 --port 8081 --root-path / --log-level debug --access-log --proxy-headers --reload api:app
50 | build:
51 | context: .
52 | dockerfile: ./deploy/docker/api/Dockerfile
53 | depends_on:
54 | - redis
55 | env_file: .env
56 | volumes:
57 | - ./config:/srv/app/config
58 | networks:
59 | - pda
60 | healthcheck:
61 | test: [ 'CMD', 'curl', '-f', 'http://pda_api:8081/status' ]
62 | interval: 30s
63 | timeout: 10s
64 | retries: 3
65 | start_period: 15s
66 | start_interval: 5s
67 |
68 | worker:
69 | image: powerdnsadmin/pda-api:latest
70 | container_name: pda_worker
71 | hostname: pda_worker
72 | restart: unless-stopped
73 | command: python3 -m celery -A worker.app worker --beat --loglevel debug
74 | build:
75 | context: .
76 | dockerfile: ./deploy/docker/api/Dockerfile
77 | depends_on:
78 | - redis
79 | env_file: .env
80 | volumes:
81 | - ./config:/srv/app/config
82 | networks:
83 | - pda
84 | healthcheck:
85 | test: [ 'CMD-SHELL', 'celery', '-A', 'worker.app', 'inspect', 'ping', '--destination', 'celery@$$HOSTNAME' ]
86 | interval: 30s
87 | timeout: 10s
88 | retries: 3
89 | start_period: 15s
90 | start_interval: 5s
91 | stop_signal: SIGTERM
92 | stop_grace_period: 900s
93 |
94 | web:
95 | image: powerdnsadmin/pda-web:latest
96 | restart: unless-stopped
97 | container_name: pda_web
98 | hostname: pda_web
99 | build:
100 | context: ./
101 | dockerfile: ./deploy/docker/web/Dockerfile
102 | env_file: .env
103 | volumes:
104 | - ./src-ui:/srv/app
105 | - ./deploy/docker/nginx/conf/web.conf:/etc/nginx/sites-enabled/default.conf
106 | - ./var/log/nginx:/var/log/nginx
107 | networks:
108 | - pda
109 | healthcheck:
110 | test: [ 'CMD', 'curl', '-f', 'http://pda_web:8000' ]
111 | interval: 30s
112 | timeout: 10s
113 | retries: 3
114 | start_period: 15s
115 | start_interval: 5s
116 |
117 | volumes:
118 | redis:
119 |
120 | networks:
121 | pda:
122 | external: false
123 | name: pda
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # For most projects, this workflow file will not need changing; you simply need
3 | # to commit it to your repository.
4 | #
5 | # You may wish to alter this file to override the set of languages analyzed,
6 | # or to provide custom queries or build logic.
7 | #
8 | # ******** NOTE ********
9 | # We have attempted to detect the languages in your repository. Please check
10 | # the `language` matrix defined below to confirm you have the correct set of
11 | # supported CodeQL languages.
12 | #
13 | name: "CodeQL"
14 |
15 | on:
16 | workflow_dispatch:
17 | push:
18 | branches:
19 | - 'dev'
20 | - 'main'
21 | - 'dependabot/**'
22 | - 'feature/**'
23 | - 'issue/**'
24 | paths:
25 | - '!src/locale/**'
26 | - '!src/requirements**'
27 | - '!src/static/css/**'
28 | - '!src/static/images/**'
29 | - '!src/.babelrc'
30 | - src/**
31 | - setup.py
32 | pull_request:
33 | # The branches below must be a subset of the branches above
34 | branches:
35 | - 'dev'
36 | - 'main'
37 | - 'dependabot/**'
38 | - 'feature/**'
39 | - 'issue/**'
40 | paths:
41 | - '!src/locale/**'
42 | - '!src/requirements**'
43 | - '!src/static/css/**'
44 | - '!src/static/images/**'
45 | - '!src/.babelrc'
46 | - src/**
47 | - setup.py
48 | schedule:
49 | - cron: '45 2 * * 2'
50 |
51 | jobs:
52 | analyze:
53 | name: Analyze
54 | runs-on: ubuntu-latest
55 | permissions:
56 | actions: read
57 | contents: read
58 | security-events: write
59 |
60 | strategy:
61 | fail-fast: false
62 | matrix:
63 | language: [ 'javascript', 'python' ]
64 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
65 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
66 |
67 | steps:
68 | - name: Checkout repository
69 | uses: actions/checkout@v3
70 |
71 | # Initializes the CodeQL tools for scanning.
72 | - name: Initialize CodeQL
73 | uses: github/codeql-action/init@v2
74 | with:
75 | languages: ${{ matrix.language }}
76 | # If you wish to specify custom queries, you can do so here or in a config file.
77 | # By default, queries listed here will override any specified in a config file.
78 | # Prefix the list here with "+" to use these queries and those in the config file.
79 |
80 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
81 | # queries: security-extended,security-and-quality
82 |
83 |
84 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
85 | # If this step fails, then you should remove it and run the build manually (see below)
86 | - name: Autobuild
87 | uses: github/codeql-action/autobuild@v2
88 |
89 | # ℹ️ Command-line programs to run using the OS shell.
90 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
91 |
92 | # If the Autobuild fails above, remove it and uncomment the following three lines.
93 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
94 |
95 | # - run: |
96 | # echo "Run, Build Application using script"
97 | # ./location_of_script_within_repo/buildscript.sh
98 |
99 | - name: Perform CodeQL Analysis
100 | uses: github/codeql-action/analyze@v2
101 |
--------------------------------------------------------------------------------
/src-api/routers/dev.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Request, Depends
2 | from fastapi.responses import JSONResponse
3 | from sqlalchemy.ext.asyncio import AsyncSession
4 | from lib.api.dependencies import get_db_session, get_principal
5 | from models.api.auth import UserSchema, ClientSchema
6 | from routers.root import router_responses
7 |
8 | router = APIRouter(
9 | prefix='/dev',
10 | responses=router_responses,
11 | )
12 |
13 |
14 | @router.get('/proxy/test')
15 | async def proxy_test(request: Request) -> JSONResponse:
16 | return JSONResponse({
17 | 'host': request.headers.get('Host'),
18 | 'x-real-ip': request.headers.get('X-Real-IP'),
19 | 'x-forwarded-for': request.headers.get('X-Forwarded-For'),
20 | 'x-forwarded-proto': request.headers.get('X-Forwarded-Proto'),
21 | 'x-forwarded-host': request.headers.get('X-Forwarded-Host'),
22 | 'x-forwarded-prefix': request.headers.get('X-Forwarded-Prefix'),
23 | 'root_path': request.scope.get('root_path'),
24 | 'request_path': request.url.path,
25 | })
26 |
27 |
28 | @router.get('/db/create')
29 | async def db_create() -> JSONResponse:
30 | import models.db # Required to properly load metadata
31 | from loguru import logger
32 | from app import db_engine
33 | from models.base import BaseSqlModel
34 |
35 | tables = BaseSqlModel.metadata.tables.keys()
36 |
37 | logger.warning(f'(Re)Creating Database Tables: {", ".join(tables)}')
38 |
39 | async with db_engine.begin() as conn:
40 | await conn.run_sync(BaseSqlModel.metadata.drop_all)
41 | await conn.run_sync(BaseSqlModel.metadata.create_all)
42 | await conn.commit()
43 |
44 | return JSONResponse({'result': 'Database Schema Created!'})
45 |
46 |
47 | @router.get('/db/test')
48 | async def db_test(session: AsyncSession = Depends(get_db_session)) -> JSONResponse:
49 | return JSONResponse({})
50 |
51 |
52 | @router.get('/auth/create-client', response_model=ClientSchema)
53 | async def auth_create_client(session: AsyncSession = Depends(get_db_session)) -> ClientSchema:
54 | """Creates an auth client."""
55 | import json
56 | from models.db.auth import Client
57 |
58 | db_client = Client(
59 | name='Test Client',
60 | scopes=json.dumps(['audit:*', 'zone:*']),
61 | )
62 |
63 | db_client.secret = 'testtest'
64 |
65 | session.add(db_client)
66 | await session.commit()
67 | await session.refresh(db_client)
68 |
69 | return ClientSchema.model_validate(db_client)
70 |
71 |
72 | @router.get('/auth/create-user', response_model=UserSchema)
73 | async def auth_create_user(session: AsyncSession = Depends(get_db_session)) -> UserSchema:
74 | """Creates an auth user."""
75 | from models.api.auth import UserSchema
76 | from models.db.auth import User, UserStatusEnum
77 |
78 | db_user = User(
79 | username='test',
80 | status=UserStatusEnum.active,
81 | )
82 |
83 | db_user.password = 'testtest'
84 |
85 | session.add(db_user)
86 | await session.commit()
87 | await session.refresh(db_user)
88 |
89 | return UserSchema.model_validate(db_user)
90 |
91 |
92 | @router.get('/auth/test/client')
93 | async def auth_test_client(principal: UserSchema | ClientSchema = Depends(get_principal)) -> JSONResponse:
94 | from loguru import logger
95 | logger.warning(principal)
96 | return JSONResponse({})
97 |
98 |
99 | @router.get('/auth/test/user')
100 | async def auth_test_user(principal: UserSchema | ClientSchema = Depends(get_principal)) -> JSONResponse:
101 | from loguru import logger
102 | logger.warning(principal)
103 | return JSONResponse({})
104 |
105 |
106 | @router.get('/acl/test')
107 | async def acl_test(principal: UserSchema | ClientSchema = Depends(get_principal)) -> JSONResponse:
108 | return JSONResponse({})
109 |
--------------------------------------------------------------------------------
/src-api/models/db/tenants.py:
--------------------------------------------------------------------------------
1 | """
2 | PDA Tenant Database Models
3 |
4 | This file defines the database models associated with tenant functionality.
5 | """
6 | import uuid
7 | from datetime import datetime
8 | from sqlalchemy import DateTime, String, Uuid, text, ForeignKey
9 | from sqlalchemy.orm import Mapped, mapped_column, relationship
10 | from models.base import BaseSqlModel
11 |
12 |
13 | class Tenant(BaseSqlModel):
14 | """Represents a tenant."""
15 |
16 | __tablename__ = 'pda_tenants'
17 | """Defines the database table name."""
18 |
19 | id: Mapped[str] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
20 | """The unique identifier of the record."""
21 |
22 | name: Mapped[str] = mapped_column(String(100), nullable=False)
23 | """The name of the tenant."""
24 |
25 | fqdn: Mapped[str] = mapped_column(String(253), nullable=True)
26 | """The FQDN for the tenant UI."""
27 |
28 | stopgap_domain_id: Mapped[str] = mapped_column(Uuid, ForeignKey('pda_stopgap_domains.id'), nullable=True)
29 | """The unique identifier of the associated stopgap domain."""
30 |
31 | stopgap_hostname: Mapped[str] = mapped_column(String(253), nullable=True)
32 | """The hostname used within the associated stopgap domain."""
33 |
34 | created_at: Mapped[datetime] = mapped_column(
35 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
36 | )
37 | """The timestamp representing when the record was created."""
38 |
39 | updated_at: Mapped[datetime] = mapped_column(
40 | DateTime, nullable=False, default=datetime.now, onupdate=datetime.now,
41 | server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP')
42 | )
43 | """The timestamp representing when the record was last updated."""
44 |
45 | stopgap_domain = relationship('StopgapDomain', back_populates='tenants')
46 | """The stopgap domain associated with the tenant."""
47 |
48 | auth_users = relationship('User', back_populates='tenant')
49 | """A list of auth users associated with the tenant."""
50 |
51 | auth_user_authenticators = relationship('UserAuthenticator', back_populates='tenant')
52 | """A list of auth user authenticators associated with the tenant."""
53 |
54 | auth_sessions = relationship('Session', back_populates='tenant')
55 | """A list of auth sessions associated with the tenant."""
56 |
57 | auth_clients = relationship('Client', back_populates='tenant')
58 | """A list of auth clients associated with the tenant."""
59 |
60 | auth_refresh_tokens = relationship('RefreshToken', back_populates='tenant')
61 | """A list of auth refresh tokens associated with the tenant."""
62 |
63 | servers = relationship('Server', back_populates='tenant')
64 | """A list of servers associated with the tenant."""
65 |
66 | auto_primaries = relationship('ServerAutoPrimary', back_populates='tenant')
67 | """A list of server auto primary registrations associated with the tenant."""
68 |
69 | views = relationship('View', back_populates='tenant')
70 | """A list of views associated with the tenant."""
71 |
72 | view_zones = relationship('ViewZone', back_populates='tenant')
73 | """A list of view zones associated with the tenant."""
74 |
75 | view_networks = relationship('ViewNetwork', back_populates='tenant')
76 | """A list of view networks associated with the tenant."""
77 |
78 | crypto_keys = relationship('CryptoKey', back_populates='tenant')
79 | """A list of cryptographic keys associated with the tenant."""
80 |
81 | tsig_keys = relationship('TsigKey', back_populates='tenant')
82 | """A list of TSIG keys associated with the tenant."""
83 |
84 | azones = relationship('AZone', back_populates='tenant')
85 | """A list of authoritative zones associated with the tenant."""
86 |
87 | rzones = relationship('RZone', back_populates='tenant')
88 | """A list of recursive zones associated with the tenant."""
89 |
--------------------------------------------------------------------------------
/src-api/lib/pda/api/services/mail.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from pydantic import Field
3 | from typing import Optional, Union
4 | from lib.mail import EmailSendRecipientResponse
5 | from models.base import BaseModel
6 |
7 |
8 | class MailServiceSendRequest(BaseModel):
9 | """Represents an API request for sending an email via the mail service router."""
10 | from_address: Optional[str] = Field(
11 | title='Email Sender Address',
12 | description='The email address to use in the mail "From" field. If left empty then the server default will be used.',
13 | examples=['Your Name ', 'admin@domain.com', 'Server '],
14 | default=None,
15 | )
16 | to_addresses: Union[list[str], str] = Field(
17 | ...,
18 | title='Email Recipients',
19 | description='One or more email addresses to deliver the mail to. May be expressed as an array of strings containing one email each or a single string with a single recipient.',
20 | examples=[['Your Name ', 'somebody@domain.com'], 'Your Name ', 'somebody@domain.com'], 'Your Name Submission Received!We want to notify you that we received your submission.
'],
40 | default=None,
41 | )
42 | body_plain: Optional[str] = Field(
43 | title='Email Plain Text Body',
44 | description='The plain text body of the email.',
45 | examples=['Submission Received!\n\nWe want to notify you that we received your submission.'],
46 | default=None,
47 | )
48 |
49 |
50 | class MailServiceSendResponse(BaseModel):
51 | """Represents an API response for sending an email via the mail service router."""
52 |
53 | id: Optional[uuid.UUID] = Field(
54 | title='Mail Request ID',
55 | description='The UUID associated with the mail request task.',
56 | examples=[str(uuid.uuid4())],
57 | default=None,
58 | )
59 |
60 | status: str = Field(
61 | title='Mail Request Status',
62 | description='The status of the mail request.',
63 | examples=['queued', 'sending', 'sent', 'retry', 'failed', 'not-found'],
64 | default='queued',
65 | )
66 |
67 | message: str = Field(
68 | title='Mail Request Response Message',
69 | description='Message related to the mail request response.',
70 | examples=['The mail request could not be queued for sending.'],
71 | default=None,
72 | )
73 |
74 | responses: list[EmailSendRecipientResponse] = Field(
75 | title='Mail Request Response Objects',
76 | description='A list of response objects of send action for each recipient.',
77 | examples=[
78 | EmailSendRecipientResponse(recipient='Recipient #1 ', success=True, code=250),
79 | EmailSendRecipientResponse(recipient='Recipient #2 ', success=False, code=450,
80 | message='This mailbox is unavailable.'),
81 | EmailSendRecipientResponse(recipient='recipient3@domain.com', success=True, code=250),
82 | ],
83 | default=None,
84 | )
85 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 |
3 | redis:
4 | image: redis:8.0.1
5 | container_name: pda_redis
6 | hostname: redis
7 | restart: unless-stopped
8 | env_file: .env
9 | volumes:
10 | - redis:/data
11 | networks:
12 | - pda
13 | healthcheck:
14 | test: [ 'CMD', 'redis-cli', '--raw', 'incr', 'ping' ]
15 | interval: 30s
16 | timeout: 10s
17 | retries: 3
18 | start_period: 15s
19 | start_interval: 5s
20 |
21 | mysql:
22 | image: mysql:8
23 | restart: unless-stopped
24 | container_name: pda_mysql
25 | volumes:
26 | - mysql:/var/lib/mysql
27 | ports:
28 | - "${PDA_SERVICE_IP}:3306:3306"
29 | networks:
30 | - pda
31 | environment:
32 | - MYSQL_ROOT_PASSWORD=${PDA_MYSQL_ROOT_PASSWORD}
33 | - MYSQL_DATABASE=${PDA_MYSQL_DATABASE}
34 |
35 | proxy:
36 | image: nginx:1.27
37 | container_name: pda_proxy
38 | hostname: pda_proxy
39 | restart: unless-stopped
40 | env_file: .env
41 | volumes:
42 | - ./deploy/docker/nginx/certs:/etc/nginx/certs
43 | - ./deploy/docker/nginx/conf/proxy.conf.template:/etc/nginx/templates/default.conf.template
44 | - ./var/log/nginx:/var/log/nginx
45 | ports:
46 | - "${PDA_SERVICE_IP}:80:80"
47 | - "${PDA_SERVICE_IP}:443:443"
48 | networks:
49 | - pda
50 | healthcheck:
51 | test: [ 'CMD', 'curl', '-f', 'http://pda_proxy:80' ]
52 | interval: 30s
53 | timeout: 10s
54 | retries: 3
55 | start_period: 15s
56 | start_interval: 5s
57 |
58 | api:
59 | image: powerdnsadmin/pda-api:latest
60 | container_name: pda_api
61 | hostname: pda_api
62 | restart: unless-stopped
63 | command: python3 -m uvicorn --host 0.0.0.0 --port 8000 --root-path / --log-level debug --access-log --proxy-headers --reload api:app
64 | build:
65 | context: .
66 | dockerfile: ./deploy/docker/api/Dockerfile
67 | depends_on:
68 | - redis
69 | env_file: .env
70 | volumes:
71 | - ./src-api:/srv/app/src
72 | - ./config:/srv/app/config
73 | networks:
74 | - pda
75 | healthcheck:
76 | test: [ 'CMD', 'curl', '-f', 'http://pda_api:8000/status' ]
77 | interval: 30s
78 | timeout: 10s
79 | retries: 3
80 | start_period: 15s
81 | start_interval: 5s
82 |
83 | worker:
84 | image: powerdnsadmin/pda-api:latest
85 | container_name: pda_worker
86 | hostname: pda_worker
87 | restart: unless-stopped
88 | command: python3 -m celery -A worker.app worker --beat --loglevel debug
89 | build:
90 | context: .
91 | dockerfile: ./deploy/docker/api/Dockerfile
92 | depends_on:
93 | - redis
94 | env_file: .env
95 | volumes:
96 | - ./src-api:/srv/app/src
97 | - ./config:/srv/app/config
98 | networks:
99 | - pda
100 | healthcheck:
101 | test: [ 'CMD-SHELL', 'celery', '-A', 'worker.app', 'inspect', 'ping', '--destination', 'celery@$$HOSTNAME' ]
102 | interval: 30s
103 | timeout: 10s
104 | retries: 3
105 | start_period: 15s
106 | start_interval: 5s
107 | stop_signal: SIGTERM
108 | stop_grace_period: 900s
109 |
110 | web:
111 | image: powerdnsadmin/pda-web:latest
112 | restart: unless-stopped
113 | container_name: pda_web
114 | hostname: pda_web
115 | build:
116 | context: ./
117 | dockerfile: ./deploy/docker/web/Dockerfile
118 | env_file: .env
119 | volumes:
120 | - ./src-ui:/srv/app
121 | - ./deploy/docker/nginx/conf/web.conf:/etc/nginx/sites-enabled/default.conf
122 | - ./var/log/nginx:/var/log/nginx
123 | networks:
124 | - pda
125 | healthcheck:
126 | test: [ 'CMD', 'curl', '-f', 'http://pda_web:8000' ]
127 | interval: 30s
128 | timeout: 10s
129 | retries: 3
130 | start_period: 15s
131 | start_interval: 5s
132 |
133 | volumes:
134 | redis:
135 | mysql:
136 |
137 | networks:
138 | pda:
139 | external: false
140 | name: pda
--------------------------------------------------------------------------------
/docs/announcements/2023-03-11-release-v0.4.0.md:
--------------------------------------------------------------------------------
1 | First off, thank you all for sticking with the project this long through it's rough patches! As the project has went through a change of ownership last year, there have been many areas that have suffered but it is my goal to turn those problems around and get the project back on a healthy and productive path into the future.
2 |
3 | With that being said, there is many areas that still need improvement including the quality of contributions and information tracking. For this reason, this release will not provide the most accurate documentation to all of the changes included in the release, but I will do my best to highlight the most notable that I'm aware of.
4 |
5 | ### Enhancements
6 | - The user interface has been updated to the latest AdminLTE release of 3.2.0.
7 | - The Font Awesome library has been upgraded to the latest 6.3.0 release which overrides the 5.15.4 dependency built in to AdminLTE 3.2.0.
8 | - The user interface has been updated to better support multiple screen sizes included smartphones and tablets. While this is still a work in progress, you will find that **most** views now work quite well in various screen formats.
9 | - The nomenclature for DNS zones have been updated to utilize the appropriate terminology of "zone" instead of "domain" where the latter is slang. This is a work in progress so there may still be some places that use the old "domain" terminology.
10 | - The nomenclature for history / action auditing has been updated to use "activity" instead of "history." This is a work in progress so not all references may have been updated yet.
11 | - The authentication settings management views have been updated to be more uniform with some amount of field documentation included. This is also still a work in progress as some views still lack useful documentation.
12 | - Performance improvements have been made to the zone record list view so that large data sets don't suffer as much from use of poor coding techniques.
13 | - The statistics and recent activity blocks have been removed from the dashboard to provide greater performance enhancements. It was determined that these features weren't truly useful in practice which was the ultimate driver for their removal.
14 | - API activity auditing has been updated to provide per-record change log entries.
15 | - The user interface has been updated to use the SITE_NAME setting in more appropriate places as opposed to the static text of "PowerDNS Admin."
16 | - Various user interface features were updated to correct minor formatting issues such as a lack of proper text wrapping for the activity details modal.
17 | - Various areas of documentation were updated to be more reliable as well as including some missing information. This is still a work in progress.
18 | - The project WIKI was moved into standard markdown documentation files under `docs/wiki` as opposed to using the GitHub WIKI feature which will make it easier for contributors to provide updates via PRs.
19 |
20 | ### Features
21 | - A CAPTCHA feature has been added to the registration form which can be controlled through the use of environment variables and application settings. This feature is now enabled by default.
22 | - A session storage setting has been added through the use of environment variables and supports multiple mediums such as database and file system. The default is configured to use the database which **will result in the "sessions" table being automatically created!**
23 | - A configuration setting has been added for controlling the OIDC OAuth Metadata URL for authentication.
24 | - A search function was added for IDN searches.
25 |
26 | ### Bug Fixes
27 | - The auto-PTR record logic has been updated to remove, then add records in order to address scenarios that would create undesirable record changes that didn't fit logical expectations.
28 | - Fixed issue where OTP_FORCE setting was being applied to OAuth flows which was not appropriate.
29 | - Many other minor bug fixes were made but there is currently a lot of lacking documentation available to make documenting these fixes here a bit easier.
30 |
31 | ### Security Fixes
32 | - Most dependencies have been updated to their latest or near-latest versions.
33 |
34 | As the project gets back on track, so will it's organization which should result in more frequent minor and patch releases with greater detail in release notes. Thank you again for being a PDA user!
35 |
36 |
This discussion was created from the release v0.4.0.
--------------------------------------------------------------------------------
/docs/announcements/2024-01-31-release-0.4.2.md:
--------------------------------------------------------------------------------
1 | This release focused on tying up what loose ends could be within reason in preparation for the freeze release. Following this release, only dependency updates within reason will be managed. There may be additional feature releases on this edition, but nothing is promised.
2 |
3 | **POTENTIALLY BREAKING CHANGE** - This release upgrades to SQLAlchemy `1.4.x` which removes support for the use of `postgres://` on database connection URI strings. You must switch to the supported format of `postgresql://` to avoid a failure of the connection.
4 |
5 | ## What's Changed
6 | * Updated the OAuth service providers to properly respect the new OAuth auto-configuration settings for each provider. by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1527
7 | * Corrected issue with `SERVER_EXTERNAL_SSL` setting not being extracted from the app's environment. by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1529
8 | * Fixed issue with all unassigned zones being selected after a new account's name fails to validate by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1530
9 | * Allow all application settings to be configured by environment variables by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1535
10 | * Fix record comment removal by @corubba in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1537
11 | * Automatically focus username field in login view by @roelschroeven in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1549
12 | * Indicate Unsaved Changes by @AgentTNT in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1595
13 | * Remove Misc Code by @AgentTNT in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1597
14 | * Fix non rr_set events in Zone Changelog display by @AgentTNT in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1598
15 | * Update static fonts to use relative paths instead of static by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1703
16 | * Fixes local user setup to perform case-insensitive verification of existing usernames / emails in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1658
17 | * Update index router to replace the use of the deprecated `before_app_first_request` event with `record_once` by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1705
18 | * Updated zone type comparison logic in domain router to be case-insensitive by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1706
19 | * Fix zone name encoding for UI XHR requests as well as requests to the PDNS API by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1707
20 | * Added LDAP search filter cleansing mechanism to properly escape special characters by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1726
21 | * Merge zone editor record action controls into single column by @feldsam in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1642
22 | * Fixing new LDAP search filter cleansing mechanism to only target user DN value returned on AD connections by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1727
23 | * Added support for application to run in sub-paths while not breaking the Docker health check by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1728
24 | * Bump mysqlclient from 2.0.1 to 2.2.1 by @dependabot in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1729
25 | * Bump bcrypt from 4.0.1 to 4.1.2 by @dependabot in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1730
26 | * Bump pytest from 7.2.1 to 7.4.4 by @dependabot in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1733
27 | * Bump sqlalchemy from 1.3.24 to 1.4.51 by @dependabot in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1734
28 | * Bump jinja2 from 3.1.2 to 3.1.3 by @dependabot in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1735
29 | * Updated Pip Dependencies (Jinaj2, certifi, cryptography, requests, werkzeug) by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1740
30 | * Bump crypto-js from 4.1.1 to 4.2.0 by @dependabot in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1738
31 | * Updated NPM dependencies (cryto-js) by @AzorianMatt in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1742
32 |
33 | ## New Contributors
34 | * @roelschroeven made their first contribution in https://github.com/PowerDNS-Admin/PowerDNS-Admin/pull/1549
35 |
36 | **Full Changelog**: https://github.com/PowerDNS-Admin/PowerDNS-Admin/compare/v0.4.1...v0.4.2
--------------------------------------------------------------------------------
/src-api/models/db/crypto.py:
--------------------------------------------------------------------------------
1 | """
2 | DNS Crypto Database Models
3 |
4 | This file defines the database models associated with DNSSEC crypto functionality.
5 | """
6 | import uuid
7 | from datetime import datetime
8 | from enum import Enum
9 | from sqlalchemy import Boolean, DateTime, Integer, String, TEXT, Uuid, text, ForeignKey
10 | from sqlalchemy.orm import Mapped, mapped_column, relationship
11 | from models.base import BaseSqlModel
12 |
13 |
14 | class CryptoKeyTypeEnum(str, Enum):
15 | """Defines the different types of DNSSEC cryptographic keys there can be."""
16 |
17 | KSK = "KSK"
18 | """DNSSEC Key Signing Key (KSK) type."""
19 |
20 | ZSK = "ZSK"
21 | """DNSSEC Zone Signing Key (ZSK) type."""
22 |
23 | CSK = "CSK"
24 | """DNSSEC Combined Signing Key (CSK) type."""
25 |
26 |
27 | class CryptoKey(BaseSqlModel):
28 | """Represents a DNSSEC cryptographic key."""
29 |
30 | __tablename__ = 'pda_crypto_keys'
31 | """Defines the database table name."""
32 |
33 | id: Mapped[str] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
34 | """The unique identifier of the record."""
35 |
36 | tenant_id: Mapped[str] = mapped_column(Uuid, ForeignKey('pda_tenants.id'), nullable=False)
37 | """The unique identifier of the tenant that owns the record."""
38 |
39 | internal_id: Mapped[int] = mapped_column(Integer, nullable=True)
40 | """The internal identifier, read only."""
41 |
42 | type_: Mapped[CryptoKeyTypeEnum] = mapped_column(String(20), nullable=False)
43 | """The type of the key."""
44 |
45 | active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
46 | """Whether the key is in active use."""
47 |
48 | published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
49 | """Whether the DNSKEY record is published in the zone."""
50 |
51 | dns_key: Mapped[str] = mapped_column(TEXT, nullable=True)
52 | """The DNSKEY record for this key."""
53 |
54 | ds: Mapped[list[str]] = mapped_column(TEXT, nullable=True)
55 | """A list of DS records for this key."""
56 |
57 | cds: Mapped[list[str]] = mapped_column(TEXT, nullable=True)
58 | """A list of DS records for this key, filtered by CDS publication settings."""
59 |
60 | private_key: Mapped[str] = mapped_column(TEXT, nullable=True)
61 | """The private key in ISC format."""
62 |
63 | algorithm: Mapped[str] = mapped_column(String(20), nullable=True)
64 | """The name of the algorithm of the key, should be a mnemonic."""
65 |
66 | bits: Mapped[int] = mapped_column(Integer, nullable=False)
67 | """The size of the key."""
68 |
69 | created_at: Mapped[datetime] = mapped_column(
70 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
71 | )
72 | """The timestamp representing when the record was created."""
73 |
74 | updated_at: Mapped[datetime] = mapped_column(
75 | DateTime, nullable=False, default=datetime.now, onupdate=datetime.now,
76 | server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP')
77 | )
78 | """The timestamp representing when the record was last updated."""
79 |
80 | tenant = relationship('Tenant', back_populates='crypto_keys')
81 | """The tenant associated with the cryptographic key."""
82 |
83 |
84 | class TsigKey(BaseSqlModel):
85 | """Represents a TSIG key."""
86 |
87 | __tablename__ = 'pda_tsig_keys'
88 | """Defines the database table name."""
89 |
90 | id: Mapped[str] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
91 | """The unique identifier of the record."""
92 |
93 | tenant_id: Mapped[str] = mapped_column(Uuid, ForeignKey('pda_tenants.id'), nullable=False)
94 | """The unique identifier of the tenant that owns the record."""
95 |
96 | internal_id: Mapped[str] = mapped_column(String(100), nullable=True)
97 | """The internal identifier, read only."""
98 |
99 | algorithm: Mapped[str] = mapped_column(String(20), nullable=True)
100 | """The algorithm of the TSIG key."""
101 |
102 | key: Mapped[str] = mapped_column(TEXT, nullable=True)
103 | """The base64 encoded secret key."""
104 |
105 | created_at: Mapped[datetime] = mapped_column(
106 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
107 | )
108 | """The timestamp representing when the record was created."""
109 |
110 | updated_at: Mapped[datetime] = mapped_column(
111 | DateTime, nullable=False, default=datetime.now, onupdate=datetime.now,
112 | server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP')
113 | )
114 | """The timestamp representing when the record was last updated."""
115 |
116 | tenant = relationship('Tenant', back_populates='tsig_keys')
117 | """The tenant associated with the TSIG key."""
118 |
--------------------------------------------------------------------------------
/src-api/lib/services/zabbix.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 | from queue import Queue
3 | from threading import Event, Thread
4 | from typing import Any
5 | from lib.config.services import ServicesConfig
6 |
7 |
8 | class ZabbixMetric:
9 | host: str = None
10 | key: str
11 | value: Any
12 | clock: int
13 | ns: int
14 |
15 | def __init__(self, key: str, value: Any, host: str = None):
16 | self.host = host
17 | self.key = key
18 | self.value = value
19 | self.set_timestamp()
20 |
21 | def __str__(self):
22 | return f'{self.host}:{self.key}:{self.value}'
23 |
24 | def set_timestamp(self, timestamp: int = None):
25 | import time
26 |
27 | if timestamp is None:
28 | timestamp = time.time_ns()
29 |
30 | self.clock = timestamp // 1_000_000_000
31 | self.ns = timestamp % 1_000_000_000
32 |
33 | def to_json(self):
34 | return {
35 | 'host': self.host,
36 | 'key': self.key,
37 | 'value': self.value,
38 | 'clock': self.clock,
39 | 'ns': self.ns,
40 | }
41 |
42 |
43 | class ZabbixSender:
44 | _config: ServicesConfig.ZabbixConfig
45 |
46 | def __init__(self, config: ServicesConfig.ZabbixConfig):
47 | self._config = config
48 |
49 | def send(self, metrics: list[ZabbixMetric]) -> str:
50 | """Send the given list of metrics to the Zabbix server."""
51 | import json, socket, struct
52 |
53 | if not self._config.reporter_enabled:
54 | return 'Reporter disabled by configuration.'
55 |
56 | if not self._config.sender_enabled:
57 | return 'Sender disabled by configuration.'
58 |
59 | payload = {
60 | 'request': 'sender data',
61 | 'data': [m.to_json() for m in metrics],
62 | }
63 | data = json.dumps(payload).encode('utf-8')
64 | header = b'ZBXD\x01' + struct.pack('This discussion was created from the release v0.4.1.
--------------------------------------------------------------------------------
/.github/workflows/mega-linter.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # MegaLinter GitHub Action configuration file
3 | # More info at https://megalinter.io
4 | name: MegaLinter
5 |
6 | on:
7 | workflow_dispatch:
8 | push:
9 | branches:
10 | - '-dev'
11 | - '-main'
12 | - '-dependabot/**'
13 | - '-feature/**'
14 | - '-issue/**'
15 | - '-release/**'
16 | tags:
17 | - '-v*.*.*'
18 | paths:
19 | - .github/**
20 | - deployment/**
21 | - docs/**
22 | - src/**
23 | - tests/**
24 | - setup.py
25 | - '*.md'
26 | pull_request:
27 | branches:
28 | - '-dev'
29 | - '-main'
30 | - '-dependabot/**'
31 | - '-feature/**'
32 | - '-issue/**'
33 | - '-release/**'
34 | tags:
35 | - '-v*.*.*'
36 | paths:
37 | - .github/**
38 | - deployment/**
39 | - docs/**
40 | - src/**
41 | - tests/**
42 | - setup.py
43 | - '*.md'
44 |
45 | env: # Comment env block if you do not want to apply fixes
46 | # Apply linter fixes configuration
47 | APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool)
48 | APPLY_FIXES_EVENT: all # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all)
49 | APPLY_FIXES_MODE: pull_request # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request)
50 |
51 | concurrency:
52 | group: ${{ github.ref }}-${{ github.workflow }}
53 | cancel-in-progress: true
54 |
55 | jobs:
56 | build:
57 | name: MegaLinter
58 | runs-on: ubuntu-latest
59 | steps:
60 | # Git Checkout
61 | - name: Checkout Code
62 | uses: actions/checkout@v3
63 | with:
64 | token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
65 |
66 | # MegaLinter
67 | - name: MegaLinter
68 | id: ml
69 | # You can override MegaLinter flavor used to have faster performances
70 | # More info at https://megalinter.io/flavors/
71 | uses: oxsecurity/megalinter@v6
72 | env:
73 | # All available variables are described in documentation
74 | # https://megalinter.io/configuration/
75 | VALIDATE_ALL_CODEBASE: true # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources
76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77 | PAT: ${{ secrets.PAT }}
78 | # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY
79 | # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks
80 |
81 | # Upload MegaLinter artifacts
82 | - name: Archive production artifacts
83 | if: ${{ success() }} || ${{ failure() }}
84 | uses: actions/upload-artifact@v3
85 | with:
86 | name: MegaLinter reports
87 | path: |
88 | megalinter-reports
89 | mega-linter.log
90 |
91 | # Create pull request if applicable (for now works only on PR from same repository, not from forks)
92 | - name: Create PR with applied fixes
93 | id: cpr
94 | if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
95 | uses: peter-evans/create-pull-request@v4
96 | with:
97 | token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
98 | commit-message: "[MegaLinter] Apply linters automatic fixes"
99 | title: "[MegaLinter] Apply linters automatic fixes"
100 | labels: bot
101 |
102 | - name: Create PR output
103 | if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
104 | run: |
105 | echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
106 | echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
107 |
108 | # Push new commit if applicable (for now works only on PR from same repository, not from forks)
109 | - name: Prepare commit
110 | if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
111 | run: sudo chown -Rc $UID .git/
112 | - name: Commit and push applied linter fixes
113 | if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
114 | uses: stefanzweifel/git-auto-commit-action@v4
115 | with:
116 | branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
117 | commit_message: "[MegaLinter] Apply linters fixes"
118 |
119 |
--------------------------------------------------------------------------------
/src-api/models/db/tasks.py:
--------------------------------------------------------------------------------
1 | """
2 | PDA Task Database Models
3 |
4 | This file defines the database models associated with task functionality.
5 | """
6 | import uuid
7 | from datetime import datetime
8 | from enum import Enum
9 | from sqlalchemy import DateTime, DECIMAL, Integer, String, TEXT, Uuid, text, ForeignKey
10 | from sqlalchemy.orm import Mapped, mapped_column, relationship
11 | from typing import Optional
12 | from models.base import BaseSqlModel
13 |
14 |
15 | class TaskJobStatusEnum(str, Enum):
16 | """Defines the different task job statuses."""
17 | received = "received"
18 | """When a task job has been received but not yet started."""
19 |
20 | revoked = "revoked"
21 | """When a task job has been revoked before completion."""
22 |
23 | running = "running"
24 | """When a task job is actively running."""
25 |
26 | retry = "retry"
27 | """When a task job has failed to complete successfully and is awaiting another attempt."""
28 |
29 | success = "success"
30 | """When a task job has completed successfully."""
31 |
32 | failed = "failed"
33 | """When a task job has failed permanently having exhausted any available retry attempts."""
34 |
35 | internal_error = "internal_error"
36 | """When a task job has failed permanently from having an internal error."""
37 |
38 |
39 | class TaskJob(BaseSqlModel):
40 | """Represents a PDA task job."""
41 |
42 | __tablename__ = 'pda_task_jobs'
43 | """Defines the database table name."""
44 |
45 | id: Mapped[str] = mapped_column(Uuid, primary_key=True)
46 | """The unique identifier of the record."""
47 |
48 | root_id: Mapped[str] = mapped_column(Uuid)
49 | """The unique identifier of the Celery root task associated with this task."""
50 |
51 | parent_id: Mapped[Optional[str]] = mapped_column(Uuid)
52 | """The unique identifier of the Celery parent task associated with this task."""
53 |
54 | task_id: Mapped[Optional[str]] = mapped_column(Uuid)
55 | """The unique identifier of the Celery task associated with this task."""
56 |
57 | name: Mapped[str] = mapped_column(String(255), nullable=False)
58 | """The name of the Celery task."""
59 |
60 | args: Mapped[Optional[str]] = mapped_column(TEXT)
61 | """The JSON-encoded arguments of the Celery task."""
62 |
63 | kwargs: Mapped[Optional[str]] = mapped_column(TEXT)
64 | """The JSON-encoded keyword arguments of the Celery task."""
65 |
66 | options: Mapped[Optional[str]] = mapped_column(TEXT)
67 | """The JSON-encoded options of the Celery task."""
68 |
69 | retries: Mapped[Optional[int]] = mapped_column(Integer, nullable=False, default=0)
70 | """The total number of execution retries performed."""
71 |
72 | runtime: Mapped[Optional[float]] = mapped_column(DECIMAL(14, 6))
73 | """The total runtime of the task job in seconds."""
74 |
75 | output: Mapped[Optional[str]] = mapped_column(TEXT)
76 | """The captured STDOUT and STDERR of the task job."""
77 |
78 | errors: Mapped[Optional[str]] = mapped_column(TEXT)
79 | """The captured exception stacktraces of the task job."""
80 |
81 | status: Mapped[TaskJobStatusEnum] = mapped_column(String(20), nullable=False)
82 | """The current status of the task job."""
83 |
84 | created_at: Mapped[datetime] = mapped_column(
85 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
86 | )
87 | """The timestamp representing when the record was created."""
88 |
89 | updated_at: Mapped[datetime] = mapped_column(
90 | DateTime, nullable=False, default=datetime.now, onupdate=datetime.now,
91 | server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP')
92 | )
93 | """The timestamp representing when the record was last updated."""
94 |
95 | started_at: Mapped[Optional[DateTime]] = mapped_column(DateTime)
96 | """The timestamp representing when the task job was started."""
97 |
98 | ended_at: Mapped[Optional[DateTime]] = mapped_column(DateTime)
99 | """The timestamp representing when the task job was completed."""
100 |
101 | activities = relationship('TaskJobActivity', back_populates='task_job')
102 | """A list of activities associated with the task job."""
103 |
104 |
105 | class TaskJobActivity(BaseSqlModel):
106 | """Represents a PDA task job activity update."""
107 |
108 | __tablename__ = 'pda_task_job_activities'
109 | """Defines the database table name."""
110 |
111 | id: Mapped[str] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
112 | """The unique identifier of the record."""
113 |
114 | task_job_id: Mapped[str] = mapped_column(Uuid, ForeignKey('pda_task_jobs.id'), nullable=False)
115 | """The unique identifier of the task job associated with this activity update."""
116 |
117 | error: Mapped[Optional[str]] = mapped_column(TEXT)
118 | """The captured exception stacktrace of a failed task job execution."""
119 |
120 | status: Mapped[TaskJobStatusEnum] = mapped_column(String(20), nullable=False)
121 | """The status of the task job for the activity update."""
122 |
123 | created_at: Mapped[datetime] = mapped_column(
124 | DateTime, nullable=False, default=datetime.now, server_default=text('CURRENT_TIMESTAMP')
125 | )
126 | """The timestamp representing when the record was created."""
127 |
128 | task_job = relationship('TaskJob', back_populates='activities')
129 | """The task job associated with the activity update."""
130 |
--------------------------------------------------------------------------------
/src-api/lib/services/microsoft/teams.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from pydantic import Field
3 | from typing import Optional, Union
4 | from lib.services.microsoft.adaptivecards import ElementTypes, ContainerStyleEnum
5 | from models.base import BaseModel
6 |
7 |
8 | class MessageActionType(str, Enum):
9 | """Represents an Adaptive Card Message Action type."""
10 | OpenUrl = "Action.OpenUrl"
11 |
12 |
13 | class MessageAction(BaseModel):
14 | """Represents a Microsoft Teams Message action."""
15 |
16 | type: MessageActionType = Field(description="The type of the action")
17 | """The type of the action."""
18 |
19 |
20 | class MessageActionOpenUrl(MessageAction):
21 | """Represents a Microsoft Teams Message URL open action."""
22 |
23 | type: MessageActionType = Field(
24 | description="The type of the action",
25 | default=MessageActionType.OpenUrl,
26 | )
27 | """The type of the action."""
28 |
29 | title: str = Field(
30 | description="The title of the action",
31 | default="View in Browser",
32 | )
33 | """The title of the action."""
34 |
35 | url: str = Field(description="The URL of the action")
36 | """The URL of the action."""
37 |
38 |
39 | ActionTypes = Union[MessageAction, MessageActionOpenUrl]
40 |
41 |
42 | class MessageAttachment(BaseModel):
43 | """Represents a Microsoft Teams message attachment."""
44 |
45 | content_type: str = Field(
46 | description="The content type of the message attachment",
47 | alias="contentType",
48 | )
49 | """The content type of the message attachment."""
50 |
51 |
52 | class AdaptiveCardMessageAttachment(MessageAttachment):
53 | """Represents a Microsoft Teams Adaptive Card message attachment."""
54 |
55 | class Content(BaseModel):
56 | """Represents a Microsoft Teams Adaptive Card message attachment content object."""
57 |
58 | schema_: str = Field(
59 | description="The schema of the content object.",
60 | alias="$schema",
61 | default="http://adaptivecards.io/schemas/adaptive-card.json",
62 | )
63 | """The schema of the content object."""
64 |
65 | type: str = Field(
66 | description="The type of the content object.",
67 | default="AdaptiveCard",
68 | )
69 | """The type of the content object."""
70 |
71 | version: str = Field(
72 | description="The version of the content object.",
73 | default="1.2",
74 | )
75 | """The version of the content object."""
76 |
77 | body: list[ElementTypes] = Field(
78 | description="The body of the content object.",
79 | default_factory=list,
80 | min_length=1,
81 | )
82 | """The body of the content object."""
83 |
84 | actions: list[ActionTypes] = Field(
85 | description="The actions attached to the content object.",
86 | default_factory=list,
87 | )
88 |
89 | content_type: str = Field(
90 | description="The content type of the message attachment",
91 | alias="contentType",
92 | default="application/vnd.microsoft.card.adaptive",
93 | )
94 | """The content type of the message attachment."""
95 |
96 | content: Content = Field(
97 | description="The content object of the message attachment",
98 | default_factory=Content,
99 | )
100 | """The content object of the message attachment."""
101 |
102 |
103 | class Message(BaseModel):
104 | """Represents a Microsoft Teams message."""
105 |
106 | type: str = Field(
107 | description="The type of the message",
108 | default="message",
109 | )
110 | """The type of the message."""
111 |
112 | attachments: list[AdaptiveCardMessageAttachment] = Field(
113 | description="A list of attachments attached to the message",
114 | default_factory=list,
115 | min_length=1,
116 | )
117 | """The list of attachments attached to the message."""
118 |
119 |
120 | class MessageFactory:
121 | """Represents a Microsoft Teams Message factory."""
122 |
123 | @staticmethod
124 | def create_simple_message(
125 | segments: list[str], title: Optional[str] = None, style: Optional[ContainerStyleEnum] = None,
126 | action_url: Optional[str] = None, action_title: Optional[str] = None,
127 | ) -> Message:
128 | """This method is used to create a simple Microsoft Teams message."""
129 | from lib.services.microsoft.adaptivecards import (
130 | Container, TextBlock, FontSizeEnum, FontWeightEnum
131 | )
132 |
133 | attachment = AdaptiveCardMessageAttachment()
134 |
135 | if isinstance(title, str) and len(stripped_title := title.strip()):
136 | container = Container()
137 |
138 | if isinstance(style, ContainerStyleEnum):
139 | container.style = style
140 |
141 | container.items.append(TextBlock(
142 | text=stripped_title,
143 | size=FontSizeEnum.medium,
144 | weight=FontWeightEnum.bolder,
145 | wrap=True,
146 | ))
147 |
148 | attachment.content.body.append(container)
149 |
150 | for segment in segments:
151 | attachment.content.body.append(TextBlock(
152 | text=segment,
153 | wrap=True,
154 | size=FontSizeEnum.medium,
155 | ))
156 |
157 | if isinstance(action_url, str) and len(stripped_action_url := action_url.strip()):
158 | action = MessageActionOpenUrl(url=stripped_action_url)
159 | if isinstance(action_title, str) and len(stripped_action_title := action_title.strip()):
160 | action.title = stripped_action_title
161 |
162 | attachment.content.actions.append(action)
163 |
164 | return Message(attachments=[attachment])
165 |
--------------------------------------------------------------------------------
/src-api/routers/v1/services/mail.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from celery.result import AsyncResult
3 | from fastapi import APIRouter
4 | from fastapi.responses import JSONResponse
5 | from lib.pda.api.services.mail import MailServiceSendRequest, MailServiceSendResponse
6 | from routers.root import router_responses
7 |
8 | router = APIRouter(
9 | prefix='/mail',
10 | tags=['services'],
11 | responses=router_responses,
12 | )
13 |
14 | responses = {
15 | 200: {
16 | 'description': 'Mail request sent.',
17 | 'content': {
18 | 'application/json': {
19 | 'example': {
20 | 'id': uuid.uuid4(),
21 | 'status': 'sent',
22 | 'message': 'Mail request completed.',
23 | 'responses': [
24 | {'recipient': 'Recipient #1 ', 'success': True, 'code': 250},
25 | {'recipient': 'Recipient #2 ', 'success': False, 'code': 450,
26 | 'message': 'This mailbox is unavailable.'},
27 | {'recipient': 'recipient3@domain.com', 'success': True, 'code': 250},
28 | ],
29 | },
30 | },
31 | },
32 | },
33 | 202: {
34 | 'description': 'Mail request queued.',
35 | 'content': {
36 | 'application/json': {
37 | 'example': {
38 | 'id': uuid.uuid4(),
39 | 'status': 'queued',
40 | 'message': 'Mail request queued.',
41 | },
42 | },
43 | },
44 | },
45 | 500: {
46 | 'description': 'Mail request failed.',
47 | 'content': {
48 | 'application/json': {
49 | 'example': {
50 | 'id': uuid.uuid4(),
51 | 'status': 'failed',
52 | 'message': 'Mail request failed.',
53 | },
54 | },
55 | },
56 | },
57 | }
58 |
59 |
60 | def update_response_from_task(response: MailServiceSendResponse, task: AsyncResult) -> tuple[
61 | MailServiceSendResponse, int]:
62 | """Updates the given response object with appropriate information based on the state of the given task."""
63 |
64 | response.status = 'queued'
65 | response.message = 'Mail request queued.'
66 | status_code = 202
67 | state = task.state
68 |
69 | if state == 'STARTED':
70 | response.status = 'sending'
71 | response.message = 'Mail request sending.'
72 |
73 | if state == 'RETRY':
74 | response.status = 'retry'
75 | response.message = 'Mail request pending retry.'
76 |
77 | if state == 'SUCCESS':
78 | response.status = 'sent'
79 | response.message = 'Mail request completed.'
80 | response.responses = task.result.responses
81 | status_code = 200
82 |
83 | if state == 'FAILURE':
84 | response.status = 'failed'
85 | response.message = 'Mail request failed.'
86 | status_code = 500
87 |
88 | return response, status_code
89 |
90 |
91 | @router.post('/send', response_model=MailServiceSendResponse, responses=responses)
92 | async def send(request: MailServiceSendRequest, wait_for_finish: bool = False, timeout: float = 60) -> JSONResponse:
93 | import time
94 | from loguru import logger
95 | from lib.enums import TaskEnum
96 | from worker import app as celery_app
97 |
98 | response = MailServiceSendResponse()
99 |
100 | try:
101 | logger.debug(f'Queueing mail request: {request}')
102 |
103 | task: AsyncResult = celery_app.send_task(TaskEnum.PDA_MAIL.value, kwargs={
104 | 'mail_from': request.from_address,
105 | 'mail_to': request.to_addresses,
106 | 'mail_cc': request.cc_addresses,
107 | 'subject': request.subject,
108 | 'body_html': request.body_html,
109 | 'body_text': request.body_plain,
110 | })
111 |
112 | response.id = uuid.UUID(task.id)
113 |
114 | if wait_for_finish:
115 | start = time.time()
116 | while time.time() - start < timeout:
117 | time.sleep(1)
118 | if task.ready():
119 | break
120 |
121 | response, status_code = update_response_from_task(response, task)
122 |
123 | return JSONResponse(response.model_dump(mode='json'), status_code=status_code)
124 |
125 | except Exception as e:
126 | logger.error(f'Mail request failed:\n{e}')
127 |
128 | response.status = 'failed'
129 | response.message = 'Mail request failed.'
130 |
131 | return JSONResponse(response.model_dump(mode='json'), status_code=500)
132 |
133 |
134 | @router.get('/status/:id', response_model=MailServiceSendResponse, responses=responses)
135 | async def status(id: uuid.UUID) -> JSONResponse:
136 | from worker import app as celery_app
137 |
138 | response = MailServiceSendResponse(id=id)
139 |
140 | task = AsyncResult(id=str(id), app=celery_app)
141 |
142 | response.status = 'queued'
143 | response.message = 'Mail request queued.'
144 | status_code = 202
145 | state = task.state
146 |
147 | if state == 'STARTED':
148 | response.status = 'sending'
149 | response.message = 'Mail request sending.'
150 |
151 | if state == 'RETRY':
152 | response.status = 'retry'
153 | response.message = 'Mail request pending retry.'
154 |
155 | if state == 'SUCCESS':
156 | response.status = 'sent'
157 | response.message = 'Mail request completed.'
158 | response.responses = task.result.responses
159 | status_code = 200
160 |
161 | if state == 'FAILURE':
162 | response.status = 'failed'
163 | response.message = 'Mail request failed.'
164 | status_code = 500
165 |
166 | return JSONResponse(response.model_dump(mode='json'), status_code=status_code)
167 |
--------------------------------------------------------------------------------
/docs/wiki/contributing/labeling-standards.md:
--------------------------------------------------------------------------------
1 | # PDA Next
2 |
3 | ## Contribution Guide
4 |
5 | ### Labeling Standards
6 |
7 | It's important to recognize the value of proper labeling for issues and discussions when a project operates at scale.
8 | Without this organization, it makes for extra workload on the project maintainers as it requires a detailed review of
9 | each unlabeled issue to set in motion any further workflows, both automated and manual.
10 |
11 | Please consider the following document when creating new issues or discussions. If an issue or discussion isn't
12 | properly labeled per the following standards, it may result in notable delays before a project maintainer addresses
13 | the issue or discussion.
14 |
15 | ### Labeling Categories
16 |
17 | ##### Bug Issues
18 |
19 | This category should be used to report any mis-behaving / broken feature, changes that break functionality or usability,
20 | and most importantly, security vulnerabilities.
21 |
22 | | Property | Value |
23 | |----------|---------|
24 | | Color | #d73a4a |
25 | | Pattern | bug / * |
26 |
27 | ##### Documentation Issues
28 |
29 | This category should be used to hold any discussions of existing project documentation as well as making new
30 | documentation requests.
31 |
32 | | Property | Value |
33 | |----------|----------|
34 | | Color | #0075ca |
35 | | Pattern | docs / * |
36 |
37 | ##### Feature Issues
38 |
39 | This category should be used to hold any discussions of existing features as well as making new feature or enhancement
40 | requests.
41 |
42 | | Property | Value |
43 | |----------|-------------|
44 | | Color | #008672 |
45 | | Pattern | feature / * |
46 |
47 |
48 | ##### Help Issues
49 |
50 | This category should be used to request help with the various aspects of using the application.
51 |
52 | | Property | Value |
53 | |----------|----------|
54 | | Color | #d876e3 |
55 | | Pattern | help / * |
56 |
57 | ##### Moderation Flags
58 |
59 | This category should only be used by project maintainers for use in management workflows.
60 |
61 | | Property | Value |
62 | |----------|---------|
63 | | Color | #e5ef23 |
64 | | Pattern | mod / * |
65 |
66 | ##### Skills Flags
67 |
68 | This category should only be used by project maintainers to categorize the skills required for addressing a topic.
69 |
70 | | Property | Value |
71 | |----------|-----------|
72 | | Color | #5319e7 |
73 | | Pattern | skill / * |
74 |
75 | ### Labels
76 |
77 | Below is a list of all labels currently used on the project for organizing issues, discussions, and pull-requests.
78 |
79 | | Label | When to use it |
80 | |------------------------------|----------------------------------------------------------------------|
81 | | bug / broken-feature | Existing feature malfunctioning or broken |
82 | | bug / security-vulnerability | Security vulnerability identified with the application |
83 | | docs / discussion | Documentation change proposals |
84 | | docs / request | Documentation change request |
85 | | feature / dependency | Existing feature dependency |
86 | | feature / discussion | New or existing feature discussion |
87 | | feature / request | New feature or enhancement request |
88 | | help / deployment | Questions regarding application deployment |
89 | | help / features | Questions regarding the use of application features |
90 | | help / other | General questions not specific to application deployment or features |
91 | | mod / accepted | This request has been accepted |
92 | | mod / announcement | This is an admin announcement |
93 | | mod / change-request | Used by internal developers to indicate a change-request. |
94 | | mod / changes-requested | Changes have been requested before proceeding |
95 | | mod / duplicate | This issue or pull request already exists |
96 | | mod / good-first-issue | Good for newcomers |
97 | | mod / help-wanted | Extra attention is needed |
98 | | mod / invalid | This doesn't seem right |
99 | | mod / rejected | This request has been rejected |
100 | | mod / reviewed | This request has been reviewed |
101 | | mod / reviewing | This request is being reviewed |
102 | | mod / stale | This request has gone stale |
103 | | mod / tested | This has been tested |
104 | | mod / testing | This is being tested |
105 | | mod / wont-fix | This will not be worked on |
106 | | skill / database | Requires a database skill-set |
107 | | skill / docker | Requires a Docker skill-set |
108 | | skill / documentation | Requires a documentation skill-set |
109 | | skill / html | Requires a HTML skill-set |
110 | | skill / javascript | Requires a JavaScript skill-set |
111 | | skill / python | Requires a Python skill-set |
112 |
--------------------------------------------------------------------------------
/src-api/tasks/pda.py:
--------------------------------------------------------------------------------
1 | from celery import current_app
2 | from typing import Any
3 | from lib.enums import TaskEnum
4 | from lib.mail import EmailSendResult
5 |
6 |
7 | @current_app.task(name=TaskEnum.PDA_MAIL.value, label='PDA Mail',
8 | autoretry_for=(Exception,), retry_kwargs={'max_retries': 1, 'countdown': 300})
9 | def mail(individual_tasks: bool = True, **kwargs) -> EmailSendResult:
10 | """This task builds an email based on the given arguments and sends it to the defined recipient(s)."""
11 | import jsonpickle
12 | import time
13 | from loguru import logger
14 | from worker import app as celery_app
15 | from lib.mail import Email
16 |
17 | if 'mail_to' not in kwargs:
18 | raise Exception('Mail recipient(s) not provided!')
19 |
20 | if not isinstance(kwargs['mail_to'], list) and not isinstance(kwargs['mail_to'], str):
21 | raise Exception(f'Mail recipient(s) not valid: Type: {type(kwargs["mail_to"])}, Value: {kwargs["mail_to"]}')
22 |
23 | if isinstance(kwargs['mail_to'], str):
24 | kwargs['mail_to'] = [kwargs['mail_to']]
25 |
26 | log_msg = 'Sending mail:\n'
27 |
28 | if 'mail_from' in kwargs:
29 | log_msg += f'From: {kwargs["mail_from"]}\n'
30 |
31 | log_msg += f'To: {kwargs["mail_to"]}\n'
32 |
33 | if 'subject' in kwargs:
34 | log_msg += f'Subject: {kwargs["subject"]}\n'
35 |
36 | if 'template' in kwargs:
37 | log_msg += f'Template: {kwargs["template"]}\n'
38 | if 'data' in kwargs:
39 | if isinstance(kwargs['data'], str):
40 | log_msg += f'Data: {jsonpickle.loads(kwargs["data"])}\n\n'
41 | elif kwargs['data'] is not None:
42 | log_msg += f'Data: {kwargs["data"]}\n\n'
43 |
44 | if 'body_html' in kwargs:
45 | log_msg += f'Body [HTML]:\n{kwargs["body_html"]}\n\n'
46 |
47 | if 'body_text' in kwargs:
48 | log_msg += f'Body [PLAIN]:\n{kwargs["body_text"]}\n\n'
49 |
50 | logger.debug(log_msg)
51 |
52 | if not individual_tasks:
53 | send_result = Email(**kwargs).send()
54 | else:
55 | tasks = []
56 |
57 | for to in kwargs['mail_to']:
58 | send_kwargs = {**kwargs, 'mail_to': to}
59 | tasks.append(celery_app.send_task(TaskEnum.PDA_MAIL_SEND.value, kwargs=send_kwargs))
60 |
61 | logger.debug(f'Waiting for {len(tasks)} mail sub-tasks to complete...')
62 |
63 | while not all(task.ready() for task in tasks):
64 | time.sleep(1)
65 |
66 | send_result = EmailSendResult()
67 |
68 | for task in tasks:
69 | send_result.responses += task.result.responses
70 |
71 | logger.debug(f'All {len(tasks)} mail sub-tasks completed.')
72 |
73 | logger.debug(f'Finished sending mail.')
74 |
75 | for response in send_result.responses:
76 | logger.debug(f'Mail Send Response: Recipient: {response.recipient}, Status: {response.success}. '
77 | + f'Code: {response.code}, Message: {response.message}')
78 |
79 | return send_result
80 |
81 |
82 | @current_app.task(name=TaskEnum.PDA_MAIL_SEND.value, label='PDA Mail Send',
83 | autoretry_for=(Exception,), retry_kwargs={'max_retries': 5, 'countdown': 300})
84 | def mail_send(**kwargs) -> EmailSendResult:
85 | """This task builds an email based on the given arguments and sends it to the defined recipient(s)."""
86 | from loguru import logger
87 | from lib.mail import Email
88 |
89 | logger.debug(f'Sending mail: {kwargs}')
90 |
91 | send_result = Email(**kwargs).send()
92 |
93 | logger.debug(f'Finished sending mail.')
94 |
95 | return send_result
96 |
97 |
98 | @current_app.task(name=TaskEnum.PDA_ALERT.value, label='PDA Alert')
99 | def alert(msg: str, info: Any = None, title: str = None):
100 | """Sends an alert to the configured administrators about runtime issues."""
101 | from app import notifications
102 | from lib.notifications import NotificationManager
103 | from lib.notifications.events import AlertEvent
104 |
105 | message = msg.strip()
106 |
107 | if isinstance(info, str):
108 | message += f'\n\nInfo:\n{info}'
109 |
110 | event = AlertEvent(
111 | title=title,
112 | message=message,
113 | exception=info,
114 | )
115 |
116 | NotificationManager(configs=notifications).handle_event(event)
117 |
118 |
119 | @current_app.task(name=TaskEnum.PDA_TEST.value, label='PDA Test Task')
120 | def test():
121 | """Sends a log message to indicate task execution."""
122 | import time
123 | from loguru import logger
124 | logger.warning(f'Starting test task.')
125 | time.sleep(1)
126 | logger.warning(f'Finished test task.')
127 |
128 |
129 | @current_app.task(name=TaskEnum.PDA_TEST_MAIL.value, label='PDA Test Mail')
130 | def test_mail():
131 | """Sends an alert message to test the mailing system."""
132 | from loguru import logger
133 | from worker import app as celery_app
134 | logger.warning(f'Starting mail test task.')
135 | celery_app.send_task(TaskEnum.PDA_ALERT.value, kwargs={
136 | 'msg': 'PDA Mail Test',
137 | 'info': 'This is an PDA mailing system test.'
138 | })
139 | logger.warning(f'Finished mail test task.')
140 |
141 |
142 | @current_app.task(name=TaskEnum.PDA_TEST_EXCEPTION.value, label='PDA Test Exception')
143 | def test_exception():
144 | """Throws an exception immediately to test global exception handling."""
145 | raise Exception('Test Exception')
146 |
147 |
148 | @current_app.task(name=TaskEnum.PDA_TEST_EXCEPTION_RETRY.value, label='PDA Test Exception Retry',
149 | autoretry_for=(Exception,), retry_kwargs={'max_retries': 2, 'countdown': 20})
150 | def test_exception_retry():
151 | """Throws an exception immediately to test auto-retry handling."""
152 | raise Exception('Test Exception')
153 |
154 |
155 | @current_app.task(name=TaskEnum.PDA_TEST_DELAY.value, label='PDA Test Delay')
156 | def test_delay():
157 | """Executes a pause to simulate a long-running task."""
158 | import time
159 | from loguru import logger
160 | logger.warning(f'Starting test delay task.')
161 | time.sleep(60)
162 | logger.warning(f'Finished test delay task.')
163 |
--------------------------------------------------------------------------------