Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 3 | import json |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 4 | import sys |
| 5 | |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 6 | from flask import Flask, Response, g, request |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 7 | from neutron.common import config |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 8 | from neutron.db.models import allowed_address_pair as models |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 9 | from neutron.objects import ports as port_obj |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 10 | from neutron.objects.port.extensions import allowedaddresspairs as aap_obj |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 11 | from neutron_lib import context |
| 12 | from neutron_lib.db import api as db_api |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 13 | from oslo_log import log as logging |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 14 | |
| 15 | config.register_common_config_options() |
| 16 | config.init(sys.argv[1:]) |
| 17 | config.setup_logging() |
| 18 | |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 19 | LOG = logging.getLogger(__name__) |
| 20 | |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 21 | app = Flask(__name__) |
| 22 | |
| 23 | |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 24 | @app.before_request |
| 25 | def fetch_context(): |
| 26 | # Skip detail data fetch if we're running health check |
| 27 | if request.path == "/health": |
| 28 | g.ctx = context.Context() |
| 29 | return |
| 30 | content_type = request.headers.get( |
| 31 | "Content-Type", "application/x-www-form-urlencoded" |
| 32 | ) |
| 33 | if content_type == "application/x-www-form-urlencoded": |
| 34 | data = request.form.to_dict() |
| 35 | g.target = json.loads(data.get("target")) |
| 36 | g.creds = json.loads(data.get("credentials")) |
| 37 | g.rule = json.loads(data.get("rule")) |
| 38 | elif content_type == "application/json": |
| 39 | data = request.json |
| 40 | g.target = data.get("target") |
| 41 | g.creds = data.get("credentials") |
| 42 | g.rule = data.get("rule") |
| 43 | g.ctx = context.Context( |
| 44 | user_id=g.creds["user_id"], project_id=g.creds["project_id"] |
| 45 | ) |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 46 | |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 47 | |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 48 | @app.route("/address-pair", methods=["POST"]) |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 49 | def enforce_address_pair(): |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 50 | """Check if allowed address pair set to valid target IP address and MAC""" |
| 51 | # Check only IP address if strict is 0 |
| 52 | strict = bool(request.args.get("strict", default=1, type=int)) |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 53 | if "attributes_to_update" not in g.target: |
| 54 | LOG.info("No attributes_to_update found, skip check.") |
| 55 | return Response("True", status=200, mimetype="text/plain") |
| 56 | elif "allowed_address_pairs" not in g.target["attributes_to_update"]: |
| 57 | LOG.info( |
| 58 | "No allowed_address_pairs in update targets " |
| 59 | f"for port {g.target['id']}, skip check." |
| 60 | ) |
| 61 | return Response("True", status=200, mimetype="text/plain") |
| 62 | if g.target.get("allowed_address_pairs", []) == []: |
| 63 | LOG.info("Empty address pair to check on, skip check.") |
| 64 | return Response("True", status=200, mimetype="text/plain") |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 65 | |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 66 | # TODO(rlin): Ideally we should limit this policy check only if its a provider network |
| 67 | |
| 68 | ports = port_obj.Port.get_objects(g.ctx, id=[g.target["id"]]) |
| 69 | if len(ports) == 0: |
| 70 | # Note(ricolin): This happens with ports that are not well defined |
| 71 | # and missing context factors like project_id. |
| 72 | # Which port usually created by services and design for internal |
| 73 | # uses. We can skip this check and avoid blocking services. |
| 74 | msg = ( |
| 75 | f"Can't fetch port {g.target['id']} with current " |
| 76 | "context, skip this check." |
| 77 | ) |
| 78 | LOG.info(msg) |
| 79 | return Response(msg, status=403, mimetype="text/plain") |
| 80 | |
| 81 | verify_address_pairs = [] |
| 82 | target_port = ports[0] |
| 83 | db_pairs = ( |
| 84 | target_port.allowed_address_pairs if target_port.allowed_address_pairs else [] |
| 85 | ) |
| 86 | target_pairs = g.target.get("allowed_address_pairs", []) |
| 87 | db_pairs_dict = {str(p.ip_address): str(p.mac_address) for p in db_pairs} |
| 88 | for pair in target_pairs: |
| 89 | if pair.get("ip_address") not in db_pairs_dict: |
| 90 | verify_address_pairs.append(pair) |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 91 | elif ( |
| 92 | strict |
| 93 | and pair.get("mac_address") |
| 94 | and db_pairs_dict[pair.get("ip_address")] != pair.get("mac_address") |
| 95 | ): |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 96 | verify_address_pairs.append(pair) |
| 97 | |
| 98 | for allowed_address_pair in verify_address_pairs: |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 99 | if strict and "mac_address" in allowed_address_pair: |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 100 | with db_api.CONTEXT_READER.using(g.ctx): |
| 101 | ports = port_obj.Port.get_objects( |
| 102 | g.ctx, |
| 103 | network_id=g.target["network_id"], |
| 104 | project_id=g.target["project_id"], |
| 105 | mac_address=allowed_address_pair["mac_address"], |
| 106 | ) |
| 107 | if len(ports) != 1: |
| 108 | msg = ( |
| 109 | "Zero or Multiple match port found with " |
| 110 | f"MAC address {allowed_address_pair['mac_address']}." |
| 111 | ) |
| 112 | LOG.info(f"{msg} Fail check.") |
| 113 | return Response(msg, status=403, mimetype="text/plain") |
| 114 | else: |
| 115 | with db_api.CONTEXT_READER.using(g.ctx): |
| 116 | ports = port_obj.Port.get_objects( |
| 117 | g.ctx, |
| 118 | network_id=g.target["network_id"], |
| 119 | project_id=g.target["project_id"], |
| 120 | ) |
| 121 | if "ip_address" in allowed_address_pair: |
| 122 | found_match = False |
| 123 | for port in ports: |
| 124 | fixed_ips = [str(fixed_ip["ip_address"]) for fixed_ip in port.fixed_ips] |
| 125 | if allowed_address_pair["ip_address"] in fixed_ips: |
| 126 | found_match = True |
| 127 | break |
| 128 | if found_match: |
| 129 | LOG.debug("Valid address pair.") |
| 130 | continue |
| 131 | msg = f"IP address not exists in network from project {g.target['project_id']}." |
| 132 | LOG.info(f"{msg} Fail check.") |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 133 | return Response( |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 134 | msg, |
| 135 | status=403, |
| 136 | mimetype="text/plain", |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 137 | ) |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 138 | LOG.info("Valid port for address pairs, passed check.") |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 139 | return Response("True", status=200, mimetype="text/plain") |
| 140 | |
| 141 | |
| 142 | @app.route("/port-update", methods=["POST"]) |
| 143 | def enforce_port_update(): |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 144 | """Check if IP or MAC has address pair dependency |
| 145 | |
| 146 | Make sure we allow update IP or MAC only if they don't |
| 147 | have any allowed address pair dependency |
| 148 | """ |
| 149 | # Check only IP address if strict is 0 |
| 150 | strict = bool(request.args.get("strict", default=1, type=int)) |
| 151 | |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 152 | if "attributes_to_update" not in g.target: |
| 153 | LOG.info("No attributes_to_update found, skip check.") |
| 154 | return Response("True", status=200, mimetype="text/plain") |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 155 | elif (not strict or ("mac_address" not in g.target["attributes_to_update"])) and ( |
Rico Lin | d87012b | 2025-01-24 22:42:26 +0800 | [diff] [blame] | 156 | "fixed_ips" not in g.target["attributes_to_update"] |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 157 | ): |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 158 | msg = "" |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 159 | LOG.info( |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 160 | f"No {'mac_address or fixed_ips' if strict else 'fixed_ips'} in " |
| 161 | f"update targets for port {g.target['id']}, skip check." |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 162 | ) |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 163 | return Response("True", status=200, mimetype="text/plain") |
| 164 | |
| 165 | with db_api.CONTEXT_READER.using(g.ctx): |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 166 | ports = port_obj.Port.get_objects(g.ctx, id=[g.target["id"]]) |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 167 | if len(ports) == 0: |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 168 | # Note(ricolin): This happens with ports that are not well defined |
| 169 | # and missing context factors like project_id. |
| 170 | # Which port usually created by services and design for internal |
| 171 | # uses. We can skip this check and avoid blocking services. |
| 172 | LOG.info( |
| 173 | f"Can't fetch port {g.target['id']} with current " |
| 174 | "context, skip this check." |
| 175 | ) |
| 176 | return Response("True", status=200, mimetype="text/plain") |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 177 | |
| 178 | fixed_ips = [str(fixed_ip["ip_address"]) for fixed_ip in ports[0].fixed_ips] |
| 179 | |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 180 | query = g.ctx.session.query(models.AllowedAddressPair).filter( |
| 181 | models.AllowedAddressPair.ip_address.in_(fixed_ips) |
| 182 | ) |
| 183 | if strict: |
| 184 | query = query.filter( |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 185 | models.AllowedAddressPair.mac_address.in_([str(ports[0].mac_address)]) |
| 186 | ) |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 187 | pairs = [ |
| 188 | aap_obj.AllowedAddressPair._load_object(context, db_obj) |
| 189 | for db_obj in query.all() |
| 190 | ] |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 191 | if len(pairs) > 0: |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 192 | msg = f"Address pairs dependency found for port: {g.target['id']}" |
| 193 | LOG.info(msg) |
| 194 | return Response(msg, status=403, mimetype="text/plain") |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 195 | LOG.info(f"Update check passed for port: {g.target['id']}") |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 196 | return Response("True", status=200, mimetype="text/plain") |
| 197 | |
| 198 | |
| 199 | @app.route("/port-delete", methods=["POST"]) |
| 200 | def enforce_port_delete(): |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 201 | # Check only IP address if strict is 0 |
| 202 | strict = bool(request.args.get("strict", default=1, type=int)) |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 203 | fixed_ips = [str(fixed_ip["ip_address"]) for fixed_ip in g.target["fixed_ips"]] |
| 204 | with db_api.CONTEXT_READER.using(g.ctx): |
Rico Lin | 7c5be7f | 2025-03-07 12:31:26 +0900 | [diff] [blame] | 205 | query = g.ctx.session.query(models.AllowedAddressPair).filter( |
| 206 | models.AllowedAddressPair.ip_address.in_(fixed_ips) |
| 207 | ) |
| 208 | if strict: |
| 209 | query = query.filter( |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 210 | models.AllowedAddressPair.mac_address.in_( |
| 211 | [str(g.target["mac_address"])] |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 212 | ) |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 213 | ) |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 214 | |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 215 | pairs = [ |
| 216 | aap_obj.AllowedAddressPair._load_object(context, db_obj) |
| 217 | for db_obj in query.all() |
| 218 | ] |
| 219 | if len(pairs) > 0: |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 220 | msg = f"Address pairs dependency found for port: {g.target['id']}" |
| 221 | LOG.info(msg) |
| 222 | return Response(msg, status=403, mimetype="text/plain") |
| 223 | |
| 224 | LOG.info(f"Delete check passed for port: {g.target['id']}") |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 225 | return Response("True", status=200, mimetype="text/plain") |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 226 | |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 227 | |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 228 | @app.route("/health", methods=["GET"]) |
| 229 | def health_check(): |
| 230 | with db_api.CONTEXT_READER.using(g.ctx): |
Rico Lin | 85f47ed | 2024-08-20 22:14:52 +0800 | [diff] [blame] | 231 | port_obj.Port.get_objects(g.ctx, id=["neutron_policy_server_health_check"]) |
Mohammed Naser | a3a92e5 | 2024-06-03 22:30:38 -0400 | [diff] [blame] | 232 | return Response(status=200) |
| 233 | |
| 234 | |
| 235 | def create_app(): |
| 236 | return app |
| 237 | |
| 238 | |
| 239 | if __name__ == "__main__": |
Rico Lin | aa2ae04 | 2024-06-25 20:32:17 +0800 | [diff] [blame] | 240 | create_app().run(host="0.0.0.0", port=9697) |