Repository: rucio/rucio

Issue:
Circular self-reference in containers
Motivation
----------
One can create a circular self-reference in containers by adding one container (c1, say) to another container (c2) and vice versa. While self-references (created by adding a container (c1, say) to itself) fail, this circular adding via two containers does not. 

```
[cale@rucioclient ~]$ rucio add-container test:c1
Added test:c1
[cale@rucioclient ~]$ rucio add-container test:c2
Added test:c2
[cale@rucioclient ~]$ rucio attach test:c2 test:c1
DIDs successfully attached to test:c2
[cale@rucioclient ~]$ rucio attach test:c1 test:c2
DIDs successfully attached to test:c1
[cale@rucioclient ~]$ rucio list-dids --recursive  test:c1

..... goes on for ever ....

^C2022-05-04 15:37:23,400	WARNING	You pressed Ctrl+C! Exiting gracefully
2022-05-04 15:37:23,427	ERROR	a bytes-like object is required, not 'str' 
```

But (correctly):
 
```
[cale@rucioclient ~]$ rucio attach test:c2 test:c2
2022-05-04 15:44:09,525	ERROR	The resource doesn't support the requested operation.
Details: Self-append is not valid!
```




Discussion:
Sidenote: An algorithm checking this would strictly only need to check container to container attachments. Dataset to container is obviously not needed.

Resolving patch:
diff --git a/lib/rucio/core/did.py b/lib/rucio/core/did.py
--- a/lib/rucio/core/did.py
+++ b/lib/rucio/core/did.py
@@ -486,7 +486,7 @@ def __add_collections_to_container(scope, name, collections, account, session):
     :param account: The account owner.
     :param session: The database session in use.
     """
-
+    container_parents = None
     condition = []
     for cond in collections:
 
@@ -520,6 +520,13 @@ def __add_collections_to_container(scope, name, collections, account, session):
         if child_type != row.did_type:
             raise exception.UnsupportedOperation("Mixed collection is not allowed: '%s:%s' is a %s(expected type: %s)" % (row.scope, row.name, row.did_type, child_type))
 
+        if child_type == DIDType.CONTAINER:
+            if container_parents is None:
+                container_parents = {(parent['scope'], parent['name']) for parent in list_all_parent_dids(scope=scope, name=name, session=session)}
+
+            if (row.scope, row.name) in container_parents:
+                raise exception.UnsupportedOperation('Circular attachment detected. %s:%s is already a parent of %s:%s', row.scope, row.name, scope, name)
+
     for c in collections:
         did_asso = models.DataIdentifierAssociation(
             scope=scope,


Test patch:
diff --git a/lib/rucio/tests/test_did.py b/lib/rucio/tests/test_did.py
--- a/lib/rucio/tests/test_did.py
+++ b/lib/rucio/tests/test_did.py
@@ -212,6 +212,19 @@ def test_remove_did_from_followed(self, mock_scope, root_account):
 
         assert rows == 0
 
+    def test_circular_attach(self, root_account, rse_factory, did_factory):
+        """ Ensure that it's not possible to create a circular attachment of containers"""
+        container1 = did_factory.make_container()
+        container2 = did_factory.make_container()
+        container3 = did_factory.make_container()
+        attach_dids(dids=[container2], account=root_account, **container1)
+        with pytest.raises(UnsupportedOperation, match='Circular attachment detected'):
+            attach_dids(dids=[container1], account=root_account, **container2)
+
+        attach_dids(dids=[container3], account=root_account, **container2)
+        with pytest.raises(UnsupportedOperation, match='Circular attachment detected'):
+            attach_dids(dids=[container1], account=root_account, **container3)
+
 
 class TestDIDApi: