Initial commit
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..6deafc2
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 120
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.gitignore
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..3db4f1b
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,27 @@
+=====================
+Neutron Policy Server
+=====================
+
+This is a simple server which can be used to manage complex Neutron policies
+which are not possible to be managed using the default Neutron ``policy.json``
+file due to the lack of programmatic control. It covers the following use
+cases:
+
+-------------------------------------------
+Allowed Address Pairs for Provider Networks
+-------------------------------------------
+
+The default Neutron policy does not allow the use of allowed address pairs for
+provider networks. However, in a use case where you need to run a highly
+available service on a provider network, you may need to use allowed address
+pairs to allow multiple instances to share the same IP address.
+
+This service intercepts the existing Neutron policy and allows the use of
+allowed address pairs for provider networks under these circumstances:
+
+- Users can modify an ``allowed_address_pairs`` attribute to their port if they
+ own another port on the same network with the same MAC & IP address.
+- Users cannot delete a port if another port on the same network has an
+ ``allowed_address_pairs`` attribute with the same MAC & IP address.
+- Users cannot modify the ``fixed_ips`` attribute of a port if another port on
+ the same network has an ``allowed_address_pairs`` attribute with the IP.
diff --git a/neutron_policy_server/__init__.py b/neutron_policy_server/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/neutron_policy_server/__init__.py
diff --git a/neutron_policy_server/wsgi.py b/neutron_policy_server/wsgi.py
new file mode 100644
index 0000000..1b33bbb
--- /dev/null
+++ b/neutron_policy_server/wsgi.py
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+
+from flask import Flask, Response, request
+from neutron.common import config
+from neutron.objects import network as network_obj
+from neutron.objects import ports as port_obj
+from neutron_lib import context
+from neutron_lib.db import api as db_api
+
+config.register_common_config_options()
+config.init(sys.argv[1:])
+config.setup_logging()
+
+app = Flask(__name__)
+
+
+@app.route("/enforce", methods=["POST"])
+def enforce():
+ data = request.json
+ rule = data.get("rule")
+ target = data.get("target")
+ creds = data.get("creds")
+
+ ctx = context.Context(user_id=creds["user_id"], project_id=creds["project_id"])
+
+ if rule == "create_port:allowed_address_pairs":
+ # TODO(mnaser): Validate this logic, ideally we should limit this policy
+ # check only if its a provider network
+ with db_api.CONTEXT_READER.using(ctx):
+ network = network_obj.Network.get_object(ctx, id=target["network_id"])
+ if network["shared"] is False:
+ return Response(status=403)
+
+ for allowed_address_pair in target.get("allowed_address_pairs", []):
+ with db_api.CONTEXT_READER.using(ctx):
+ ports = port_obj.Port.get_objects(
+ ctx,
+ network_id=target["network_id"],
+ project_id=target["project_id"],
+ mac_address=allowed_address_pair["mac_address"],
+ )
+
+ if len(ports) != 1:
+ return Response(status=403)
+
+ fixed_ips = [str(fixed_ip["ip_address"]) for fixed_ip in ports[0].fixed_ips]
+ if allowed_address_pair["ip_address"] not in fixed_ips:
+ return Response(status=403)
+
+ return Response(status=200)
+
+
+def create_app():
+ return app
+
+
+if __name__ == "__main__":
+ create_app().run(host="0.0.0.0", port=8080)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..617a4fc
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+Flask
+neutron
+neutron-lib
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..a44cc3b
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,34 @@
+[metadata]
+name = neutron-policy-server
+summary = Advanced policy server for Neutron
+description_file =
+ README.rst
+author = VEXXHOST, Inc.
+author_email = support@vexxhost.com
+url = https://vexxhost.github.io/neutron-policy-server/
+project_urls =
+ Bug Tracker = https://github.com/vexxhost/neutron-policy-server/issues
+ Documentation = https://vexxhost.github.io/neutron-policy-server/
+ Source Code = https://github.com/vexxhost/neutron-policy-server
+python_requires = >=3.10
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Environment :: OpenStack
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ License :: OSI Approved :: Apache Software License
+ Operating System :: POSIX :: Linux
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: Implementation :: CPython
+
+[files]
+packages =
+ neutron_policy_server
+
+[entry_points]
+wsgi_scripts =
+ neutron-policy-server-wsgi = neutron_policy_server.wsgi:create_app
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..f7cabb3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: Apache-2.0
+
+import setuptools
+
+setuptools.setup(setup_requires=["pbr>=2.0.0"], pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..1c83fde
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1 @@
+PyMySQL
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..b67293f
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+minversion = 3.18.0
+
+[testenv]
+usedevelop = True
+deps =
+ -r{toxinidir}/test-requirements.txt
+
+[testenv:venv]
+deps =
+ {[testenv]deps}
+commands =
+ {posargs}