diff --git a/irrd/conf/__init__.py b/irrd/conf/__init__.py index 9260ed2..1d7bcb0 100644 --- a/irrd/conf/__init__.py +++ b/irrd/conf/__init__.py @@ -284,6 +284,7 @@ def _validate_subconfig(key, value): expected_access_lists = { config.get("server.whois.access_list"), config.get("server.http.status_access_list"), + config.get("server.http.maintainer_users_access_list"), } if not self._check_is_str(config, "email.from") or "@" not in config.get("email.from", ""): @@ -303,6 +304,7 @@ def _validate_subconfig(key, value): "email.footer", "server.whois.access_list", "server.http.status_access_list", + "server.http.maintainer_users_access_list", "rpki.notify_invalid_subject", "rpki.notify_invalid_header", "rpki.slurm_source", diff --git a/irrd/conf/known_keys.py b/irrd/conf/known_keys.py index 8da45fa..83e7f94 100644 --- a/irrd/conf/known_keys.py +++ b/irrd/conf/known_keys.py @@ -17,6 +17,7 @@ "interface": {}, "port": {}, "status_access_list": {}, + "maintainer_users_access_list": {}, "event_stream_access_list": {}, "workers": {}, "forwarded_allowed_ips": {}, diff --git a/irrd/server/http/app.py b/irrd/server/http/app.py index 5916bac..291aad9 100644 --- a/irrd/server/http/app.py +++ b/irrd/server/http/app.py @@ -29,6 +29,7 @@ StatusEndpoint, SuspensionSubmissionEndpoint, WhoisQueryEndpoint, + MaintainerUsersEndpoint, ) from irrd.server.http.event_stream import ( EventStreamEndpoint, @@ -116,6 +117,7 @@ async def shutdown(): Mount("/v1/whois", WhoisQueryEndpoint), Mount("/v1/submit", ObjectSubmissionEndpoint), Mount("/v1/suspension", SuspensionSubmissionEndpoint), + Mount("/v1/mntner_users/{source}/{mntner}", MaintainerUsersEndpoint), Mount("/graphql", graphql), Mount("/ui", name="ui", routes=UI_ROUTES), Mount("/static", name="static", app=StaticFiles(directory=STATIC_DIR)), diff --git a/irrd/server/http/endpoints_api.py b/irrd/server/http/endpoints_api.py index 6692ffa..2aad0ae 100644 --- a/irrd/server/http/endpoints_api.py +++ b/irrd/server/http/endpoints_api.py @@ -6,6 +6,8 @@ import pydantic from asgiref.sync import sync_to_async from IPy import IP +from irrd.storage.models import AuthMntner, AuthPermission, AuthUser +from irrd.storage.orm_provider import ORMSessionProvider from starlette.endpoints import HTTPEndpoint from starlette.requests import Request from starlette.responses import JSONResponse, PlainTextResponse, Response @@ -132,3 +134,34 @@ async def post(self, request: Request) -> Response: handler = ChangeSubmissionHandler() await sync_to_async(handler.load_suspension_submission)(data=data, request_meta=request_meta) return JSONResponse(handler.submitter_report_json()) + + +class MaintainerUsersEndpoint(HTTPEndpoint): + async def get(self, request: Request) -> Response: + host = request.client.host if request.client else STARLETTE_TEST_CLIENT_HOST + if not is_client_permitted(host, "server.http.maintainer_users_access_list"): + return PlainTextResponse("Access denied", status_code=403) + + session_provider = ORMSessionProvider() + target = session_provider.session.query(AuthMntner).filter_by( + rpsl_mntner_pk=request.path_params["mntner"], + rpsl_mntner_source=request.path_params["source"], + ) + mntner = await session_provider.run(target.one) + if not mntner: + return PlainTextResponse(status_code=404) + + target = session_provider.session.query(AuthPermission).filter_by( + mntner=mntner, + ).join(AuthUser) + permissions = await session_provider.run(target.all) + + return JSONResponse({ + "users": [{ + "user_management": permission.user_management, + "user_id": str(permission.user.pk), + "email": permission.user.email, + "name": permission.user.name, + "oidc_sub": permission.user.oidc_sub, + } for permission in permissions], + }) \ No newline at end of file