├── 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 | 11 | 12 | 13 |
9 | {{ title }} 10 |
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 | 15 | 16 | 17 |
13 | *** {{ mode }} Version *** 14 |
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 | --------------------------------------------------------------------------------