blob: 5fbc642e8d3ea3ac0b041fd32010c6e7d6cea5a6 [file] [log] [blame]
Mohammed Nasera3a92e52024-06-03 22:30:38 -04001# SPDX-License-Identifier: Apache-2.0
2
Rico Linaa2ae042024-06-25 20:32:17 +08003import json
Mohammed Nasera3a92e52024-06-03 22:30:38 -04004import sys
5
Rico Linaa2ae042024-06-25 20:32:17 +08006from flask import Flask, Response, g, request
Mohammed Nasera3a92e52024-06-03 22:30:38 -04007from neutron.common import config
Rico Linaa2ae042024-06-25 20:32:17 +08008from neutron.db.models import allowed_address_pair as models
Mohammed Nasera3a92e52024-06-03 22:30:38 -04009from neutron.objects import ports as port_obj
Rico Linaa2ae042024-06-25 20:32:17 +080010from neutron.objects.port.extensions import allowedaddresspairs as aap_obj
Mohammed Nasera3a92e52024-06-03 22:30:38 -040011from neutron_lib import context
12from neutron_lib.db import api as db_api
Rico Lin85f47ed2024-08-20 22:14:52 +080013from oslo_log import log as logging
Mohammed Nasera3a92e52024-06-03 22:30:38 -040014
15config.register_common_config_options()
16config.init(sys.argv[1:])
17config.setup_logging()
18
Rico Lin85f47ed2024-08-20 22:14:52 +080019LOG = logging.getLogger(__name__)
20
Mohammed Nasera3a92e52024-06-03 22:30:38 -040021app = Flask(__name__)
22
23
Rico Linaa2ae042024-06-25 20:32:17 +080024@app.before_request
25def 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 Nasera3a92e52024-06-03 22:30:38 -040046
Mohammed Nasera3a92e52024-06-03 22:30:38 -040047
Rico Lind87012b2025-01-24 22:42:26 +080048@app.route("/address-pair", methods=["POST"])
Rico Linaa2ae042024-06-25 20:32:17 +080049def enforce_address_pair():
Rico Lin7c5be7f2025-03-07 12:31:26 +090050 """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 Lind87012b2025-01-24 22:42:26 +080053 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 Nasera3a92e52024-06-03 22:30:38 -040065
Rico Lind87012b2025-01-24 22:42:26 +080066 # 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 Lin7c5be7f2025-03-07 12:31:26 +090091 elif (
92 strict
93 and pair.get("mac_address")
94 and db_pairs_dict[pair.get("ip_address")] != pair.get("mac_address")
95 ):
Rico Lind87012b2025-01-24 22:42:26 +080096 verify_address_pairs.append(pair)
97
98 for allowed_address_pair in verify_address_pairs:
Rico Lin7c5be7f2025-03-07 12:31:26 +090099 if strict and "mac_address" in allowed_address_pair:
Rico Lind87012b2025-01-24 22:42:26 +0800100 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 Linaa2ae042024-06-25 20:32:17 +0800133 return Response(
Rico Lind87012b2025-01-24 22:42:26 +0800134 msg,
135 status=403,
136 mimetype="text/plain",
Rico Linaa2ae042024-06-25 20:32:17 +0800137 )
Rico Lind87012b2025-01-24 22:42:26 +0800138 LOG.info("Valid port for address pairs, passed check.")
Rico Linaa2ae042024-06-25 20:32:17 +0800139 return Response("True", status=200, mimetype="text/plain")
140
141
142@app.route("/port-update", methods=["POST"])
143def enforce_port_update():
Rico Lin7c5be7f2025-03-07 12:31:26 +0900144 """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 Lind87012b2025-01-24 22:42:26 +0800152 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 Lin7c5be7f2025-03-07 12:31:26 +0900155 elif (not strict or ("mac_address" not in g.target["attributes_to_update"])) and (
Rico Lind87012b2025-01-24 22:42:26 +0800156 "fixed_ips" not in g.target["attributes_to_update"]
Rico Linaa2ae042024-06-25 20:32:17 +0800157 ):
Rico Lin7c5be7f2025-03-07 12:31:26 +0900158 msg = ""
Rico Lin85f47ed2024-08-20 22:14:52 +0800159 LOG.info(
Rico Lin7c5be7f2025-03-07 12:31:26 +0900160 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 Lin85f47ed2024-08-20 22:14:52 +0800162 )
Rico Linaa2ae042024-06-25 20:32:17 +0800163 return Response("True", status=200, mimetype="text/plain")
164
165 with db_api.CONTEXT_READER.using(g.ctx):
Rico Lin85f47ed2024-08-20 22:14:52 +0800166 ports = port_obj.Port.get_objects(g.ctx, id=[g.target["id"]])
Rico Linaa2ae042024-06-25 20:32:17 +0800167 if len(ports) == 0:
Rico Lin85f47ed2024-08-20 22:14:52 +0800168 # 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 Linaa2ae042024-06-25 20:32:17 +0800177
178 fixed_ips = [str(fixed_ip["ip_address"]) for fixed_ip in ports[0].fixed_ips]
179
Rico Lin7c5be7f2025-03-07 12:31:26 +0900180 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 Linaa2ae042024-06-25 20:32:17 +0800185 models.AllowedAddressPair.mac_address.in_([str(ports[0].mac_address)])
186 )
Rico Lin7c5be7f2025-03-07 12:31:26 +0900187 pairs = [
188 aap_obj.AllowedAddressPair._load_object(context, db_obj)
189 for db_obj in query.all()
190 ]
Rico Linaa2ae042024-06-25 20:32:17 +0800191 if len(pairs) > 0:
Rico Lin85f47ed2024-08-20 22:14:52 +0800192 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 Lin85f47ed2024-08-20 22:14:52 +0800195 LOG.info(f"Update check passed for port: {g.target['id']}")
Rico Linaa2ae042024-06-25 20:32:17 +0800196 return Response("True", status=200, mimetype="text/plain")
197
198
199@app.route("/port-delete", methods=["POST"])
200def enforce_port_delete():
Rico Lin7c5be7f2025-03-07 12:31:26 +0900201 # Check only IP address if strict is 0
202 strict = bool(request.args.get("strict", default=1, type=int))
Rico Linaa2ae042024-06-25 20:32:17 +0800203 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 Lin7c5be7f2025-03-07 12:31:26 +0900205 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 Linaa2ae042024-06-25 20:32:17 +0800210 models.AllowedAddressPair.mac_address.in_(
211 [str(g.target["mac_address"])]
Mohammed Nasera3a92e52024-06-03 22:30:38 -0400212 )
Rico Linaa2ae042024-06-25 20:32:17 +0800213 )
Mohammed Nasera3a92e52024-06-03 22:30:38 -0400214
Rico Linaa2ae042024-06-25 20:32:17 +0800215 pairs = [
216 aap_obj.AllowedAddressPair._load_object(context, db_obj)
217 for db_obj in query.all()
218 ]
219 if len(pairs) > 0:
Rico Lin85f47ed2024-08-20 22:14:52 +0800220 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 Linaa2ae042024-06-25 20:32:17 +0800225 return Response("True", status=200, mimetype="text/plain")
Mohammed Nasera3a92e52024-06-03 22:30:38 -0400226
Mohammed Nasera3a92e52024-06-03 22:30:38 -0400227
Rico Linaa2ae042024-06-25 20:32:17 +0800228@app.route("/health", methods=["GET"])
229def health_check():
230 with db_api.CONTEXT_READER.using(g.ctx):
Rico Lin85f47ed2024-08-20 22:14:52 +0800231 port_obj.Port.get_objects(g.ctx, id=["neutron_policy_server_health_check"])
Mohammed Nasera3a92e52024-06-03 22:30:38 -0400232 return Response(status=200)
233
234
235def create_app():
236 return app
237
238
239if __name__ == "__main__":
Rico Linaa2ae042024-06-25 20:32:17 +0800240 create_app().run(host="0.0.0.0", port=9697)