Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding certificate validation for ssl in mongo hook #37214

Merged
merged 6 commits into from
Feb 8, 2024

Conversation

amoghrajesh
Copy link
Contributor

Currently, we are skipping tls certificate validation in mongo hook even if ssl is set to true. This needs to be handled better


^ Add meaningful description above
Read the Pull Request Guidelines for more information.
In case of fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
In case of a new dependency, check compliance with the ASF 3rd Party License Policy.
In case of backwards incompatible changes please leave a note in a newsfragment file, named {pr_number}.significant.rst or {issue_number}.significant.rst, in newsfragments.

Copy link
Contributor Author

@amoghrajesh amoghrajesh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still in the process of testing it

@amoghrajesh amoghrajesh marked this pull request as draft February 7, 2024 06:22
@amoghrajesh
Copy link
Contributor Author

Right now I am testing with the DAG:

from airflow import DAG
from airflow.providers.mongo.hooks.mongo import MongoHook
from airflow.operators.python_operator import PythonOperator
from datetime import datetime, timedelta

default_args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'start_date': datetime(2024, 2, 7),
    'catchup': False,
}


def retrieve_users_from_mongodb():
    mongo_hook = MongoHook(conn_id='mongo_conn')
    query = {}
    users_data = mongo_hook.find('users', query)
    for user in users_data:
        print(user)


with DAG('mongo_dag', default_args=default_args, schedule_interval=None) as dag:
    # Task to retrieve users from MongoDB
    retrieve_users_task = PythonOperator(
        task_id='retrieve_users_from_mongodb',
        python_callable=retrieve_users_from_mongodb
    )

    # Define the task dependencies
    retrieve_users_task

Connection:
image

For some reason, the connection is not going through, to mongodb

Error log:

e8786b12ac04
*** Found local files:
***   * /root/airflow/logs/dag_id=mongo_dag/run_id=manual__2024-02-07T07:07:35.163848+00:00/task_id=retrieve_users_from_mongodb/attempt=1.log
[2024-02-07, 12:37:37 IST] {taskinstance.py:1980} INFO - Dependencies all met for dep_context=non-requeueable deps ti=
[2024-02-07, 12:37:37 IST] {taskinstance.py:1980} INFO - Dependencies all met for dep_context=requeueable deps ti=
[2024-02-07, 12:37:37 IST] {taskinstance.py:2194} INFO - Starting attempt 1 of 1
[2024-02-07, 12:37:37 IST] {taskinstance.py:2215} INFO - Executing  on 2024-02-07 07:07:35.163848+00:00
[2024-02-07, 12:37:37 IST] {standard_task_runner.py:60} INFO - Started process 1243 to run task
[2024-02-07, 12:37:37 IST] {standard_task_runner.py:87} INFO - Running: ['airflow', 'tasks', 'run', 'mongo_dag', 'retrieve_users_from_mongodb', 'manual__2024-02-07T07:07:35.163848+00:00', '--job-id', '920', '--raw', '--subdir', 'DAGS_FOLDER/mongo_test.py', '--cfg-path', '/tmp/tmpkg6apbfb']
[2024-02-07, 12:37:37 IST] {standard_task_runner.py:88} INFO - Job 920: Subtask retrieve_users_from_mongodb
[2024-02-07, 12:37:37 IST] {task_command.py:423} INFO - Running  on host e8786b12ac04
[2024-02-07, 12:37:37 IST] {taskinstance.py:2515} INFO - Exporting env vars: AIRFLOW_CTX_DAG_EMAIL='airflow@example.com' AIRFLOW_CTX_DAG_OWNER='airflow' AIRFLOW_CTX_DAG_ID='mongo_dag' AIRFLOW_CTX_TASK_ID='retrieve_users_from_mongodb' AIRFLOW_CTX_EXECUTION_DATE='2024-02-07T07:07:35.163848+00:00' AIRFLOW_CTX_TRY_NUMBER='1' AIRFLOW_CTX_DAG_RUN_ID='manual__2024-02-07T07:07:35.163848+00:00'
[2024-02-07, 12:37:37 IST] {base.py:83} INFO - Using connection ID 'myconn' for task execution.
[2024-02-07, 12:38:08 IST] {taskinstance.py:2737} ERROR - Task failed with exception
Traceback (most recent call last):
  File "/opt/airflow/airflow/models/taskinstance.py", line 446, in _execute_task
    result = _execute_callable(context=context, **execute_callable_kwargs)
  File "/opt/airflow/airflow/models/taskinstance.py", line 416, in _execute_callable
    return execute_callable(context=context, **execute_callable_kwargs)
  File "/opt/airflow/airflow/operators/python.py", line 199, in execute
    return_value = self.execute_callable()
  File "/opt/airflow/airflow/operators/python.py", line 216, in execute_callable
    return self.python_callable(*self.op_args, **self.op_kwargs)
  File "/files/dags/mongo_test.py", line 28, in retrieve_users_from_mongodb
    for user in users_data:
  File "/usr/local/lib/python3.8/site-packages/pymongo/cursor.py", line 1264, in next
    if len(self.__data) or self._refresh():
  File "/usr/local/lib/python3.8/site-packages/pymongo/cursor.py", line 1155, in _refresh
    self.__session = self.__collection.database.client._ensure_session()
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1823, in _ensure_session
    return self.__start_session(True, causal_consistency=False)
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1766, in __start_session
    self._topology._check_implicit_session_support()
  File "/usr/local/lib/python3.8/site-packages/pymongo/topology.py", line 573, in _check_implicit_session_support
    self._check_session_support()
  File "/usr/local/lib/python3.8/site-packages/pymongo/topology.py", line 589, in _check_session_support
    self._select_servers_loop(
  File "/usr/local/lib/python3.8/site-packages/pymongo/topology.py", line 259, in _select_servers_loop
    raise ServerSelectionTimeoutError(
pymongo.errors.ServerSelectionTimeoutError: 127.0.0.1:27017: [Errno 111] Connection refused (configured timeouts: socketTimeoutMS: 20000.0ms, connectTimeoutMS: 20000.0ms), Timeout: 30s, Topology Description: ]>
[2024-02-07, 12:38:08 IST] {taskinstance.py:1152} INFO - Marking task as FAILED. dag_id=mongo_dag, task_id=retrieve_users_from_mongodb, execution_date=20240207T070735, start_date=20240207T070737, end_date=20240207T070808
[2024-02-07, 12:38:08 IST] {standard_task_runner.py:107} ERROR - Failed to execute job 920 for task retrieve_users_from_mongodb (127.0.0.1:27017: [Errno 111] Connection refused (configured timeouts: socketTimeoutMS: 20000.0ms, connectTimeoutMS: 20000.0ms), Timeout: 30s, Topology Description: ]>; 1243)
[2024-02-07, 12:38:08 IST] {local_task_job_runner.py:234} INFO - Task exited with return code 1
[2024-02-07, 12:38:08 IST] {taskinstance.py:3318} INFO - 0 downstream tasks scheduled from follow-on schedule check

Point to note is that when I use pymongo directly, it works

from pymongo import MongoClient

mongo_host = "127.0.0.1"
mongo_port = 27017
mongo_database = "test"
mongo_collection = "users" 

client = MongoClient(mongo_host, mongo_port)

db = client[mongo_database]
collection = db[mongo_collection]

query_result = collection.find_one({"name": "John"})
print("Query result:", query_result)

@amoghrajesh
Copy link
Contributor Author

Also tried adding mongo://127.0.0.1, this doesnt work either for uri

@amoghrajesh amoghrajesh marked this pull request as ready for review February 7, 2024 08:14
@Taragolis
Copy link
Contributor

Also tried adding mongo://127.0.0.1, this doesnt work either for uri

I've wondering, did you run Airflow into the docker?

@amoghrajesh
Copy link
Contributor Author

amoghrajesh commented Feb 7, 2024

Also tried adding mongo://127.0.0.1, this doesnt work either for uri

I've wondering, did you run Airflow into the docker?

I am running it using breeze, so yes, you might be right.
Let me try it using docker compose

@amoghrajesh
Copy link
Contributor Author

Thanks @Taragolis !
That tip saved hassle for me, ran it via docker compose and it worked :D

@amoghrajesh
Copy link
Contributor Author

Awesome. I was able to test it.

Results:

My connections defined:
image

Logs for my_mong (ssl is not present):

c47a4cad3064
*** Found local files:
***   * /root/airflow/logs/dag_id=mongo_dag/run_id=manual__2024-02-07T09:32:50.030512+00:00/task_id=retrieve_users_from_mongodb/attempt=1.log
[2024-02-07, 15:02:53 IST] {taskinstance.py:1980} INFO - Dependencies all met for dep_context=non-requeueable deps ti=
[2024-02-07, 15:02:53 IST] {taskinstance.py:1980} INFO - Dependencies all met for dep_context=requeueable deps ti=
[2024-02-07, 15:02:53 IST] {taskinstance.py:2194} INFO - Starting attempt 1 of 1
[2024-02-07, 15:02:53 IST] {taskinstance.py:2215} INFO - Executing  on 2024-02-07 09:32:50.030512+00:00
[2024-02-07, 15:02:53 IST] {standard_task_runner.py:60} INFO - Started process 1073 to run task
[2024-02-07, 15:02:53 IST] {standard_task_runner.py:87} INFO - Running: ['airflow', 'tasks', 'run', 'mongo_dag', 'retrieve_users_from_mongodb', 'manual__2024-02-07T09:32:50.030512+00:00', '--job-id', '929', '--raw', '--subdir', 'DAGS_FOLDER/mongo_test.py', '--cfg-path', '/tmp/tmp1dqi4wjk']
[2024-02-07, 15:02:53 IST] {standard_task_runner.py:88} INFO - Job 929: Subtask retrieve_users_from_mongodb
[2024-02-07, 15:02:53 IST] {task_command.py:423} INFO - Running  on host c47a4cad3064
[2024-02-07, 15:02:53 IST] {taskinstance.py:2515} INFO - Exporting env vars: AIRFLOW_CTX_DAG_EMAIL='airflow@example.com' AIRFLOW_CTX_DAG_OWNER='airflow' AIRFLOW_CTX_DAG_ID='mongo_dag' AIRFLOW_CTX_TASK_ID='retrieve_users_from_mongodb' AIRFLOW_CTX_EXECUTION_DATE='2024-02-07T09:32:50.030512+00:00' AIRFLOW_CTX_TRY_NUMBER='1' AIRFLOW_CTX_DAG_RUN_ID='manual__2024-02-07T09:32:50.030512+00:00'
[2024-02-07, 15:02:53 IST] {base.py:83} INFO - Using connection ID 'my_mong' for task execution.
[2024-02-07, 15:02:53 IST] {logging_mixin.py:188} INFO - {'_id': ObjectId('65c34e0e7e0c274d333d8922'), 'username': 'user1', 'email': 'user1@example.com'}
[2024-02-07, 15:02:53 IST] {logging_mixin.py:188} INFO - {'_id': ObjectId('65c34e0e7e0c274d333d8923'), 'username': 'user2', 'email': 'user2@example.com'}
[2024-02-07, 15:02:53 IST] {python.py:201} INFO - Done. Returned value was: None
[2024-02-07, 15:02:53 IST] {taskinstance.py:1152} INFO - Marking task as SUCCESS. dag_id=mongo_dag, task_id=retrieve_users_from_mongodb, execution_date=20240207T093250, start_date=20240207T093253, end_date=20240207T093253
[2024-02-07, 15:02:53 IST] {local_task_job_runner.py:234} INFO - Task exited with return code 0
[2024-02-07, 15:02:53 IST] {taskinstance.py:3318} INFO - 0 downstream tasks scheduled from follow-on schedule check

Logs for my_mong_secure (explicitly passed my ssl: true)

c47a4cad3064
*** Found local files:
***   * /root/airflow/logs/dag_id=mongo_dag/run_id=manual__2024-02-07T09:36:02.449095+00:00/task_id=retrieve_users_from_mongodb/attempt=1.log
[2024-02-07, 15:06:05 IST] {taskinstance.py:1980} INFO - Dependencies all met for dep_context=non-requeueable deps ti=
[2024-02-07, 15:06:05 IST] {taskinstance.py:1980} INFO - Dependencies all met for dep_context=requeueable deps ti=
[2024-02-07, 15:06:05 IST] {taskinstance.py:2194} INFO - Starting attempt 1 of 1
[2024-02-07, 15:06:05 IST] {taskinstance.py:2215} INFO - Executing  on 2024-02-07 09:36:02.449095+00:00
[2024-02-07, 15:06:05 IST] {standard_task_runner.py:60} INFO - Started process 1397 to run task
[2024-02-07, 15:06:05 IST] {standard_task_runner.py:87} INFO - Running: ['airflow', 'tasks', 'run', 'mongo_dag', 'retrieve_users_from_mongodb', 'manual__2024-02-07T09:36:02.449095+00:00', '--job-id', '930', '--raw', '--subdir', 'DAGS_FOLDER/mongo_test.py', '--cfg-path', '/tmp/tmpn18c8wnm']
[2024-02-07, 15:06:05 IST] {standard_task_runner.py:88} INFO - Job 930: Subtask retrieve_users_from_mongodb
[2024-02-07, 15:06:05 IST] {task_command.py:423} INFO - Running  on host c47a4cad3064
[2024-02-07, 15:06:05 IST] {taskinstance.py:2515} INFO - Exporting env vars: AIRFLOW_CTX_DAG_EMAIL='airflow@example.com' AIRFLOW_CTX_DAG_OWNER='airflow' AIRFLOW_CTX_DAG_ID='mongo_dag' AIRFLOW_CTX_TASK_ID='retrieve_users_from_mongodb' AIRFLOW_CTX_EXECUTION_DATE='2024-02-07T09:36:02.449095+00:00' AIRFLOW_CTX_TRY_NUMBER='1' AIRFLOW_CTX_DAG_RUN_ID='manual__2024-02-07T09:36:02.449095+00:00'
[2024-02-07, 15:06:05 IST] {base.py:83} INFO - Using connection ID 'my_mong_secure' for task execution.
[2024-02-07, 15:06:35 IST] {taskinstance.py:2737} ERROR - Task failed with exception
Traceback (most recent call last):
  File "/opt/airflow/airflow/models/taskinstance.py", line 446, in _execute_task
    result = _execute_callable(context=context, **execute_callable_kwargs)
  File "/opt/airflow/airflow/models/taskinstance.py", line 416, in _execute_callable
    return execute_callable(context=context, **execute_callable_kwargs)
  File "/opt/airflow/airflow/operators/python.py", line 199, in execute
    return_value = self.execute_callable()
  File "/opt/airflow/airflow/operators/python.py", line 216, in execute_callable
    return self.python_callable(*self.op_args, **self.op_kwargs)
  File "/files/dags/mongo_test.py", line 28, in retrieve_users_from_mongodb
    for user in users_data:
  File "/usr/local/lib/python3.8/site-packages/pymongo/cursor.py", line 1264, in next
    if len(self.__data) or self._refresh():
  File "/usr/local/lib/python3.8/site-packages/pymongo/cursor.py", line 1155, in _refresh
    self.__session = self.__collection.database.client._ensure_session()
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1823, in _ensure_session
    return self.__start_session(True, causal_consistency=False)
  File "/usr/local/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1766, in __start_session
    self._topology._check_implicit_session_support()
  File "/usr/local/lib/python3.8/site-packages/pymongo/topology.py", line 573, in _check_implicit_session_support
    self._check_session_support()
  File "/usr/local/lib/python3.8/site-packages/pymongo/topology.py", line 589, in _check_session_support
    self._select_servers_loop(
  File "/usr/local/lib/python3.8/site-packages/pymongo/topology.py", line 259, in _select_servers_loop
    raise ServerSelectionTimeoutError(
pymongo.errors.ServerSelectionTimeoutError: SSL handshake failed: 172.20.0.3:27017: TLS/SSL connection has been closed (EOF) (_ssl.c:1131) (configured timeouts: socketTimeoutMS: 20000.0ms, connectTimeoutMS: 20000.0ms), Timeout: 30s, Topology Description: ]>
[2024-02-07, 15:06:35 IST] {taskinstance.py:1152} INFO - Marking task as FAILED. dag_id=mongo_dag, task_id=retrieve_users_from_mongodb, execution_date=20240207T093602, start_date=20240207T093605, end_date=20240207T093635
[2024-02-07, 15:06:35 IST] {standard_task_runner.py:107} ERROR - Failed to execute job 930 for task retrieve_users_from_mongodb (SSL handshake failed: 172.20.0.3:27017: TLS/SSL connection has been closed (EOF) (_ssl.c:1131) (configured timeouts: socketTimeoutMS: 20000.0ms, connectTimeoutMS: 20000.0ms), Timeout: 30s, Topology Description: ]>; 1397)
[2024-02-07, 15:06:35 IST] {local_task_job_runner.py:234} INFO - Task exited with return code 1
[2024-02-07, 15:06:35 IST] {taskinstance.py:3318} INFO - 0 downstream tasks scheduled from follow-on schedule check

cc @Taragolis @potiuk

@Taragolis
Copy link
Contributor

That tip saved hassle for me, ran it via docker compose and it worked :D

Yeah in breeze only available simple integration, which need to be run with --integration mongo, e.g.

breeze start-airflow --backend postgres --postgres-version 15 --integration mongo

After that you could communicate with Mongo 3 thought the host mongo

from airflow import DAG
from airflow.decorators import task
from airflow.providers.mongo.hooks.mongo import MongoHook


with DAG("pr_37214", schedule=None, tags=["37214", "mongodb"]):
    @task
    def test_mongo():
        # Create a connection into the runtime, might be replaced by connection created into the UI
        import os
        from airflow.models.connection import Connection

        mongo_conn_id = "mongo_test"
        conn = Connection(conn_id=mongo_conn_id, conn_type="mongo", host="mongo", port=27017, schema="test")
        os.environ[f"AIRFLOW_CONN_{conn.conn_id.upper()}"] = conn.as_json()

        # Actual execution
        mongo_hook = MongoHook(mongo_conn_id=mongo_conn_id)
        mongo_hook.insert_one("users", {"name": "John", "surname": "Wick"}, mongo_db="test")

        collection = mongo_hook.get_collection("users", mongo_db="test")
        print(collection.count_documents({}))

    test_mongo()

And it is expected that if you try to communicate in breeze with localhost or 127.0.0.1 that is not exists, because there is no mongo in Airflow Breeze container

@amoghrajesh
Copy link
Contributor Author

That tip saved hassle for me, ran it via docker compose and it worked :D

Yeah in breeze only available simple integration, which need to be run with --integration mongo, e.g.

❯ breeze start-airflow --backend postgres --postgres-version 15 --integration mongo

After that you could communicate with Mongo 3 thought the host mongo

from airflow import DAG
from airflow.decorators import task
from airflow.providers.mongo.hooks.mongo import MongoHook


with DAG("pr_37214", schedule=None, tags=["37214", "mongodb"]):
    @task
    def test_mongo():
        # Create a connection into the runtime, might be replaced by connection created into the UI
        import os
        from airflow.models.connection import Connection

        mongo_conn_id = "mongo_test"
        conn = Connection(conn_id=mongo_conn_id, conn_type="mongo", host="mongo", port=27017, schema="test")
        os.environ[f"AIRFLOW_CONN_{conn.conn_id.upper()}"] = conn.as_json()

        # Actual execution
        mongo_hook = MongoHook(mongo_conn_id=mongo_conn_id)
        mongo_hook.insert_one("users", {"name": "John", "surname": "Wick"}, mongo_db="test")

        collection = mongo_hook.get_collection("users", mongo_db="test")
        print(collection.count_documents({}))

    test_mongo()

And it is expected that if you try to communicate in breeze with localhost or 127.0.0.1 that is not exists, because there is no mongo in Airflow Breeze container

Perfect! I wasn't aware of this, thank you!
This is good :)

@amoghrajesh
Copy link
Contributor Author

@potiuk to maintain the older behaviour, I think one thing we can do is:
in init:

    if "ssl" is in self.extra and if "allow_insecure" is not in self.extras:
        self.extras["allow_insecure"] = False

and in get_conn:

    allow_insecure = get allow_insecure from options

    if options.get("ssl", False):
        if pymongo.__version__ >= "4.0.0":
            # For pymongo 4.0.0+, use 'tlsAllowInvalidCertificates'
            options["tlsAllowInvalidCertificates"] = allow_insecure
        else:
            # For older pymongo versions, use 'ssl_cert_reqs'
            options["ssl_cert_reqs"] = CERT_NONE if allow_insecure else CERT_REQUIRED

Thoughts?

@potiuk
Copy link
Member

potiuk commented Feb 7, 2024

I think having "alllow_insecure" while setting "ssl=True" is precisely the vulnerability here, because it is unexpected. So i think when user sets "ssl=True" then allow_insecure should be false by default.

And it's ok to make breaking change while fixing security bug. We want people to find out they have security issue and make them fix it.

@amoghrajesh
Copy link
Contributor Author

Sure, thanks for your review.

I will take a look at this soon, mostly tomorrow ist morning

@amoghrajesh
Copy link
Contributor Author

@potiuk this has been tested out with the following combinations:

{
  "mongo_http": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": "{\"ssl\": \"false\"}"
  },
  "mongo_http_copy1": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": "{\"ssl\": \"false\"}"
  },
  "mongo_https_allow_insecure": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": "{\"ssl\": \"true\", \"allow_insecure\": \"true\"}"
  },
  "mongo_https_dont_allow_insecure": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": "{\"ssl\": \"true\", \"allow_insecure\": \"false\"}"
  },
  "old_no_ssl_set": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": ""
  },
  "old_ssl_false": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": "{\"ssl\": \"false\"}"
  },
  "old_ssl_true": {
    "conn_type": "mongo",
    "description": "",
    "login": "",
    "password": null,
    "host": "172.20.0.3",
    "port": 27017,
    "schema": "test",
    "extra": "{\"ssl\": \"true\"}"
  }
}

It works as expected.

Copy link
Member

@potiuk potiuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really Nice. Only docs/spellcheck fix and you can merge it.

@amoghrajesh
Copy link
Contributor Author

Really Nice. Only docs/spellcheck fix and you can merge it.

Just ran it locally and pushed a fix, we should be good!

@potiuk potiuk merged commit a5a6549 into apache:main Feb 8, 2024
56 checks passed
Comment on lines +30 to +31
3.7.0
......
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post merge comment:

I guess the version should be 4.0.0 in case of breaking changes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah .. Right 🤦

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will raise a follow up for this. Sorry missed it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants