Root cause
Authentication & access control
The new registry covered every gateway create, read, update and delete route and the endpoint bindings list, while the secrets, endpoints and model definitions list operations were left without an entry.
How AI contributed: Flawed AI-written codePublished Jun 2, 2026 in
mlflow/mlflowAll 7 findings in this repository
The change that added permission checks for gateway CRUD operations wrote the gateway authorization registry from scratch: validators for reading, creating, updating and deleting secrets, endpoints and model definitions, plus the endpoint bindings and tags, including the endpoint bindings list route. The three list operations were left out of that registry, so they stayed reachable without a permission check. The repair registers after-request filters for those three operations, and each filter parses the response, keeps only the rows the caller is allowed to read and passes admins through untouched.
An authenticated user without admin rights sends a GET to the gateway secrets, endpoints or model definitions list route on a server started with the basic-auth app. Those three operation names have no entry in the before-request handler registry, so the lookup finds no validator and the request reaches the list handler, which reads every row from the tracking store and returns them unfiltered. The result is full enumeration of gateway secrets, endpoint configurations and model definitions by any account that can sign in, because these routes never consult per-resource permissions.
Root cause
The new registry covered every gateway create, read, update and delete route and the endpoint bindings list, while the secrets, endpoints and model definitions list operations were left without an entry.
Fix
The three list operations are now registered against after-request filters that drop rows the caller cannot read, and new coverage tests assert the dropped rows.
Case facts
@@ -703,6 +909,30 @@ BEFORE_REQUEST_HANDLERS = { GetScorer: validate_can_read_scorer, DeleteScorer: validate_can_delete_scorer, ListScorerVersions: validate_can_read_scorer,+ # Routes for gateway secrets+ GetGatewaySecretInfo: validate_can_read_gateway_secret,+ UpdateGatewaySecret: validate_can_update_gateway_secret,+ DeleteGatewaySecret: validate_can_delete_gateway_secret,+ # Routes for gateway endpoints+ CreateGatewayEndpoint: validate_can_create_gateway_endpoint,+ GetGatewayEndpoint: validate_can_read_gateway_endpoint,+ UpdateGatewayEndpoint: validate_can_update_gateway_endpoint,+ DeleteGatewayEndpoint: validate_can_delete_gateway_endpoint,+ # Routes for gateway model definitions+ CreateGatewayModelDefinition: validate_can_create_gateway_model_definition,+ GetGatewayModelDefinition: validate_can_read_gateway_model_definition,+ UpdateGatewayModelDefinition: validate_can_update_gateway_model_definition,+ DeleteGatewayModelDefinition: validate_can_delete_gateway_model_definition,+ # Routes for gateway endpoint-model mappings+ AttachModelToGatewayEndpoint: validate_can_update_gateway_endpoint,+ DetachModelFromGatewayEndpoint: validate_can_update_gateway_endpoint,+ # Routes for gateway endpoint bindings+ CreateGatewayEndpointBinding: validate_can_update_gateway_endpoint,+ DeleteGatewayEndpointBinding: validate_can_update_gateway_endpoint,+ ListGatewayEndpointBindings: validate_can_read_gateway_endpoint,+ # Routes for gateway endpoint tags+ SetGatewayEndpointTag: validate_can_update_gateway_endpoint,+ DeleteGatewayEndpointTag: validate_can_update_gateway_endpoint, } @@ -4140,6 +4167,50 @@ def filter_list_scorers(resp: Response) -> None: resp.data = message_to_json(response_message) +# The list endpoints reach the handler behind the gateway-proxy validator (authenticated);+# these after-request filters are the row-level access control, dropping rows the caller+# cannot read. Keep them registered in AFTER_REQUEST_PATH_HANDLERS.+def filter_list_gateway_endpoints(resp: Response) -> None:+ """Filter ``ListGatewayEndpoints`` responses to endpoints the caller can read."""+ if sender_is_admin():+ return+ response_message = ListGatewayEndpoints.Response()+ parse_dict(resp.json, response_message)+ can_read = _role_based_read_predicate(authenticate_request().username, "gateway_endpoint")+ kept = [row for row in response_message.endpoints if can_read(row.endpoint_id)]+ response_message.ClearField("endpoints")+ response_message.endpoints.extend(kept)+ resp.data = message_to_json(response_message)+++def filter_list_gateway_model_definitions(resp: Response) -> None:+ """Filter ``ListGatewayModelDefinitions`` responses to rows the caller can read."""+ if sender_is_admin():+ return+ response_message = ListGatewayModelDefinitions.Response()+ parse_dict(resp.json, response_message)+ can_read = _role_based_read_predicate(+ authenticate_request().username, "gateway_model_definition"+ )+ kept = [row for row in response_message.model_definitions if can_read(row.model_definition_id)]+ response_message.ClearField("model_definitions")+ response_message.model_definitions.extend(kept)+ resp.data = message_to_json(response_message)+++def filter_list_gateway_secrets(resp: Response) -> None:+ """Filter ``ListGatewaySecretInfos`` responses to secrets the caller can read."""+ if sender_is_admin():+ return+ response_message = ListGatewaySecretInfos.Response()+ parse_dict(resp.json, response_message)+ can_read = _role_based_read_predicate(authenticate_request().username, "gateway_secret")+ kept = [row for row in response_message.secrets if can_read(row.secret_id)]+ response_message.ClearField("secrets")+ response_message.secrets.extend(kept)+ resp.data = message_to_json(response_message)++ AFTER_REQUEST_PATH_HANDLERS = { CreateExperiment: set_can_manage_experiment_permission, CreateRegisteredModel: set_can_manage_registered_model_permission,@@ -4159,6 +4230,10 @@ AFTER_REQUEST_PATH_HANDLERS = { DeleteGatewayEndpoint: delete_gateway_endpoint_permissions_cascade, CreateGatewayModelDefinition: set_can_manage_gateway_model_definition_permission, DeleteGatewayModelDefinition: delete_gateway_model_definition_permissions_cascade,+ # Cross-resource gateway list endpoints: filter rows to what the caller can read.+ ListGatewayEndpoints: filter_list_gateway_endpoints,+ ListGatewayModelDefinitions: filter_list_gateway_model_definitions,+ ListGatewaySecretInfos: filter_list_gateway_secrets, ListWorkspaces: filter_list_workspaces, CreateWorkspace: _seed_default_workspace_roles, DeleteWorkspace: _cleanup_workspace_permissions,