Repository: rucio/rucio

Issue:
Support protocols with NULL priority
### Description

Rucio should treat protocol priority `NULL` the same as `0`. Though a lack of value is acceptable by the database, some parts of the code are not implemented in such a way.

### Motivation

The choice is having priority `0` meaning disabled is widely considered as unfortunate. It would have been better if `NULL` meant disabled, `0` was the top priority, `1` the next etc. Changing this design without introducing operational woes would mean splitting it into two steps and spreading them over different LTS releases.

This is the first step and should be included in Rucio 32.

### Change

* Undo the changes done for #4237.
* Review all places where protocols are manipulated and treat `NULL` the same as `0`.

**WARNING:** Adding support is one thing. Actually switching the existing protocol priorities will break backward compatibility with Rucio clients prior to 32.0.0.


Discussion:

Resolving patch:
diff --git a/lib/rucio/core/replica.py b/lib/rucio/core/replica.py
--- a/lib/rucio/core/replica.py
+++ b/lib/rucio/core/replica.py
@@ -739,10 +739,10 @@ def _get_list_replicas_protocols(
 
     rse_info = rsemgr.get_rse_info(rse_id=rse_id, session=session)
     # compute scheme priorities, and don't forget to exclude disabled protocols
-    # 0 in RSE protocol definition = disabled, 1 = highest priority
+    # 0 or None in RSE protocol definition = disabled, 1 = highest priority
     scheme_priorities = {
-        'wan': {p['scheme']: p['domains']['wan']['read'] for p in rse_info['protocols'] if p['domains']['wan']['read'] > 0},
-        'lan': {p['scheme']: p['domains']['lan']['read'] for p in rse_info['protocols'] if p['domains']['lan']['read'] > 0},
+        'wan': {p['scheme']: p['domains']['wan']['read'] for p in rse_info['protocols'] if p['domains']['wan']['read']},
+        'lan': {p['scheme']: p['domains']['lan']['read'] for p in rse_info['protocols'] if p['domains']['lan']['read']},
     }
 
     rse_schemes = copy.copy(schemes) if schemes else []
diff --git a/lib/rucio/core/rse.py b/lib/rucio/core/rse.py
--- a/lib/rucio/core/rse.py
+++ b/lib/rucio/core/rse.py
@@ -1255,12 +1255,16 @@ def list_rse_usage_history(rse_id, source=None, *, session: "Session"):
 
 
 @transactional_session
-def add_protocol(rse_id, parameter, *, session: "Session"):
+def add_protocol(
+    rse_id: str,
+    parameter: Dict[str, Any],
+    *,
+    session: "Session"
+) -> models.RSEProtocols:
     """
-    Add a protocol to an existing RSE. If entries with equal or less priority for
-    an operation exist, the existing one will be reorded (i.e. +1).
+    Add a protocol to an existing RSE.
 
-    :param rse_id: the id of the new rse.
+    :param rse_id: the ID of the new RSE.
     :param parameter: parameters of the new protocol entry.
     :param session: The database session in use.
 
@@ -1285,20 +1289,18 @@ def add_protocol(rse_id, parameter, *, session: "Session"):
     parameter['hostname'] = parameter.get('hostname', 'localhost')
 
     # Transform nested domains to match DB schema e.g. [domains][lan][read] => [read_lan]
-    if 'domains' in parameter.keys():
-        for s in parameter['domains']:
-            if s not in utils.rse_supported_protocol_domains():
-                raise exception.RSEProtocolDomainNotSupported('The protocol domain \'%s\' is not defined in the schema.' % s)
-            for op in parameter['domains'][s]:
+    if 'domains' in parameter:
+        for domain in parameter['domains']:
+            if domain not in utils.rse_supported_protocol_domains():
+                raise exception.RSEProtocolDomainNotSupported(f"The protocol domain '{domain}' is not defined in the schema.")
+            for op in parameter['domains'][domain]:
                 if op not in utils.rse_supported_protocol_operations():
-                    raise exception.RSEOperationNotSupported('Operation \'%s\' not defined in schema.' % (op))
-                op_name = op if op.startswith('third_party_copy') else ''.join([op, '_', s]).lower()
-                try:
-                    if parameter['domains'][s][op] < 0:
-                        raise exception.RSEProtocolPriorityError('The provided priority (%s)for operation \'%s\' in domain \'%s\' is not supported.' % (parameter['domains'][s][op], op, s))
-                except TypeError:
-                    raise exception.RSEProtocolPriorityError('The provided priority (%s)for operation \'%s\' in domain \'%s\' is not supported.' % (parameter['domains'][s][op], op, s))
-                parameter[op_name] = parameter['domains'][s][op]
+                    raise exception.RSEOperationNotSupported(f"Operation '{op}' not defined in schema.")
+                op_name = op if op.startswith('third_party_copy') else f'{op}_{domain}'.lower()
+                priority = parameter['domains'][domain][op]
+                if (type(priority) is not int or priority < 0) and priority is not None:
+                    raise exception.RSEProtocolPriorityError(f"The provided priority ({priority}) for operation '{op}' in domain '{domain}' is not supported.")
+                parameter[op_name] = priority
         del parameter['domains']
 
     if ('extended_attributes' in parameter) and parameter['extended_attributes']:
@@ -1474,12 +1476,12 @@ def update_protocols(
     port: int,
     *,
     session: "Session"
-):
+) -> None:
     """
     Update an existing protocol entry for an RSE.
 
     :param rse_id: the ID of the RSE.
-    :param scheme: Protocol identifer.
+    :param scheme: Protocol identifier.
     :param data: Dict with new values (keys must match column names in the database).
     :param hostname: Hostname defined for the scheme, used if more than one scheme
                      is registered with the same identifier.
@@ -1499,25 +1501,17 @@ def update_protocols(
 
     # Transform nested domains to match DB schema e.g. [domains][lan][read] => [read_lan]
     if 'domains' in data:
-        for s in data['domains']:
-            if s not in utils.rse_supported_protocol_domains():
-                raise exception.RSEProtocolDomainNotSupported('The protocol domain \'%s\' is not defined in the schema.' % s)
-            for op in data['domains'][s]:
+        for domain in data['domains']:
+            if domain not in utils.rse_supported_protocol_domains():
+                raise exception.RSEProtocolDomainNotSupported(f"The protocol domain '{domain}' is not defined in the schema.")
+            for op in data['domains'][domain]:
                 if op not in utils.rse_supported_protocol_operations():
-                    raise exception.RSEOperationNotSupported('Operation \'%s\' not defined in schema.' % (op))
-                op_name = op
-                if not op.startswith('third_party_copy'):
-                    op_name = ''.join([op, '_', s])
-                stmt = select(
-                    func.count(models.RSEProtocols.rse_id)
-                ).where(
-                    models.RSEProtocols.rse_id == rse_id,
-                    getattr(models.RSEProtocols, op_name) >= 0
-                )
-                no = session.execute(stmt).scalar()
-                if not 0 <= data['domains'][s][op] <= no:
-                    raise exception.RSEProtocolPriorityError('The provided priority (%s)for operation \'%s\' in domain \'%s\' is not supported.' % (data['domains'][s][op], op, s))
-                data[op_name] = data['domains'][s][op]
+                    raise exception.RSEOperationNotSupported(f"Operation '{op}' not defined in schema.")
+                op_name = op if op.startswith('third_party_copy') else f'{op}_{domain}'.lower()
+                priority = data['domains'][domain][op]
+                if (type(priority) is not int or priority < 0) and priority is not None:
+                    raise exception.RSEProtocolPriorityError(f"The provided priority ({priority}) for operation '{op}' in domain '{domain}' is not supported.")
+                data[op_name] = priority
         del data['domains']
 
     if 'extended_attributes' in data:
@@ -1567,7 +1561,7 @@ def del_protocols(
     port: Optional[int] = None,
     *,
     session: "Session"
-):
+) -> None:
     """
     Delete one or more existing protocol entries for an RSE.
 
diff --git a/lib/rucio/rse/rsemanager.py b/lib/rucio/rse/rsemanager.py
--- a/lib/rucio/rse/rsemanager.py
+++ b/lib/rucio/rse/rsemanager.py
@@ -101,10 +101,10 @@ def _get_possible_protocols(rse_settings, operation, scheme=None, domain=None, i
 
         if not domain:
             for d in list(protocol['domains'].keys()):
-                if protocol['domains'][d][operation] != 0:
+                if protocol['domains'][d][operation]:
                     filtered = False
         else:
-            if protocol['domains'].get(domain, {operation: 0}).get(operation) != 0:
+            if protocol['domains'].get(domain, {operation: None}).get(operation):
                 filtered = False
 
         if filtered:


Test patch:
diff --git a/lib/rucio/tests/test_bad_replica.py b/lib/rucio/tests/test_bad_replica.py
--- a/lib/rucio/tests/test_bad_replica.py
+++ b/lib/rucio/tests/test_bad_replica.py
@@ -447,7 +447,7 @@ def test_get_pfn_to_rse(rse_factory, rse_client, vo):
                   'domains': {
                       'lan': {'read': 4,
                               'write': 1,
-                              'delete': 0}
+                              'delete': None}
                   },
                   'extended_attributes': 'TheOneWithAllTheRest'},
                  {'scheme': 'MOCK',
@@ -458,7 +458,7 @@ def test_get_pfn_to_rse(rse_factory, rse_client, vo):
                   'domains': {
                       'lan': {'read': 1,
                               'write': 1,
-                              'delete': 0}},
+                              'delete': None}},
                   'extended_attributes': 'TheOneWithAllTheRest'}, ]
     for prot in protocols:
         rse_client.add_protocol(rse1, prot)
diff --git a/lib/rucio/tests/test_replica.py b/lib/rucio/tests/test_replica.py
--- a/lib/rucio/tests/test_replica.py
+++ b/lib/rucio/tests/test_replica.py
@@ -27,7 +27,7 @@
 from werkzeug.datastructures import Headers
 
 from rucio.client.ruleclient import RuleClient
-from rucio.common.exception import (DataIdentifierNotFound, AccessDenied, RSEProtocolPriorityError, RucioException,
+from rucio.common.exception import (DataIdentifierNotFound, AccessDenied, RucioException,
                                     ReplicaIsLocked, ReplicaNotFound, ScopeNotFound,
                                     DatabaseException, InputValidationError)
 from rucio.common.schema import get_schema_value
@@ -449,27 +449,6 @@ def test_list_replica_with_schemes(self, rse_factory, mock_scope, root_account,
         replicas = list(replica_client.list_replicas([{'scope': mock_scope.external, 'name': name}]))
         assert 'http://' in list(replicas[0]['pfns'].keys())[0]
 
-        with pytest.raises(RSEProtocolPriorityError):
-            # check that the appropriate exception is raised when an invalid setting is passed
-            add_protocol(rse_id, {'scheme': 'http',
-                                  'hostname': 'http.aperture.com',
-                                  'port': 80,
-                                  'prefix': '//test/chamber/',
-                                  'impl': 'rucio.rse.protocols.gfal.Default',
-                                  'domains': {
-                                      'lan': {'read': None, 'write': 1, 'delete': 1},  # None should be int >= 0
-                                      'wan': {'read': 1, 'write': 1, 'delete': 1}}})
-        with pytest.raises(RSEProtocolPriorityError):
-            # check that the appropriate exception is raised when an invalid setting is passed
-            add_protocol(rse_id, {'scheme': 'http',
-                                  'hostname': 'http.aperture.com',
-                                  'port': 80,
-                                  'prefix': '//test/chamber/',
-                                  'impl': 'rucio.rse.protocols.gfal.Default',
-                                  'domains': {
-                                      'lan': {'read': 1, 'write': 1, 'delete': 1},  # None should be int >= 0
-                                      'wan': {'read': None, 'write': 1, 'delete': 1}}})
-
     def test_replica_no_site(self, rse_factory, mock_scope, root_account, replica_client):
         """ REPLICA (CORE): Test listing replicas without site attribute """
 
diff --git a/lib/rucio/tests/test_replica_sorting.py b/lib/rucio/tests/test_replica_sorting.py
--- a/lib/rucio/tests/test_replica_sorting.py
+++ b/lib/rucio/tests/test_replica_sorting.py
@@ -144,7 +144,7 @@ def protocols_setup(vo):
                                      'prefix': '/test/chamber/',
                                      'impl': 'rucio.rse.protocols.gfal.Default',
                                      'domains': {
-                                         'lan': {'read': 0, 'write': 0, 'delete': 0},
+                                         'lan': {'read': None, 'write': None, 'delete': None},
                                          'wan': {'read': 3, 'write': 3, 'delete': 3}}})
 
     add_protocol(rse_info[1]['id'], {'scheme': 'gsiftp',
@@ -161,7 +161,7 @@ def protocols_setup(vo):
                                      'prefix': '/lambda/complex/',
                                      'impl': 'rucio.rse.protocols.gfal.Default',
                                      'domains': {
-                                         'lan': {'read': 0, 'write': 0, 'delete': 0},
+                                         'lan': {'read': None, 'write': None, 'delete': None},
                                          'wan': {'read': 2, 'write': 2, 'delete': 2}}})
     add_protocol(rse_info[1]['id'], {'scheme': 'root',
                                      'hostname': ('root.%s' % base_rse_info[1]['address']),
diff --git a/lib/rucio/tests/test_rse.py b/lib/rucio/tests/test_rse.py
--- a/lib/rucio/tests/test_rse.py
+++ b/lib/rucio/tests/test_rse.py
@@ -511,7 +511,7 @@ def test_add_protocol(self, vo, rucio_client):
                       'domains': {
                           'lan': {'read': 4,
                                   'write': 1,
-                                  'delete': 0}
+                                  'delete': None}
                       },
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK',
@@ -522,7 +522,7 @@ def test_add_protocol(self, vo, rucio_client):
                       'domains': {
                           'lan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK',
                       'hostname': 'localhost',
@@ -532,7 +532,7 @@ def test_add_protocol(self, vo, rucio_client):
                       'domains': {
                           'lan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK',
                       'hostname': 'localhost',
@@ -542,7 +542,7 @@ def test_add_protocol(self, vo, rucio_client):
                       'domains': {
                           'lan': {'read': 2,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
         for p in protocols:
             rucio_client.add_protocol(protocol_rse, p)
@@ -834,8 +834,8 @@ def test_get_protocols(self, vo, rucio_client):
                           'lan': {'read': 1,
                                   'write': 1,
                                   'delete': 1},
-                          'wan': {'read': 0,
-                                  'write': 0,
+                          'wan': {'read': None,
+                                  'write': None,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK_WRITE',
@@ -844,10 +844,10 @@ def test_get_protocols(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1},
-                          'wan': {'read': 0,
+                          'wan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
@@ -857,8 +857,8 @@ def test_get_protocols(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
-                                  'write': 0,
+                          'lan': {'read': None,
+                                  'write': None,
                                   'delete': 1},
                           'wan': {'read': 1,
                                   'write': 1,
@@ -902,7 +902,7 @@ def test_get_protocols_operations(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
@@ -912,8 +912,8 @@ def test_get_protocols_operations(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
-                                  'write': 0,
+                          'lan': {'read': None,
+                                  'write': None,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
         # Protocol identifier include supported operations
@@ -1023,7 +1023,7 @@ def test_get_protocols_operations_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
@@ -1033,7 +1033,7 @@ def test_get_protocols_operations_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
@@ -1043,8 +1043,8 @@ def test_get_protocols_operations_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
-                                  'write': 0,
+                          'lan': {'read': None,
+                                  'write': None,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
         # Protocol for read is undefined
@@ -1069,7 +1069,7 @@ def test_get_protocols_domain_not_exist(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}
@@ -1093,7 +1093,7 @@ def test_get_protocols_domain_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
@@ -1119,7 +1119,7 @@ def test_get_protocols_defaults_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
@@ -1129,7 +1129,7 @@ def test_get_protocols_defaults_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
+                          'lan': {'read': None,
                                   'write': 1,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
@@ -1139,8 +1139,8 @@ def test_get_protocols_defaults_not_supported(self, vo, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
-                                  'write': 0,
+                          'lan': {'read': None,
+                                  'write': None,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
         # Protocol for read is undefined
@@ -1169,7 +1169,7 @@ def test_update_protocols_port_exist(self, rucio_client):
                       'domains': {
                           'wan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK',
                       'hostname': 'localhost',
@@ -1179,7 +1179,7 @@ def test_update_protocols_port_exist(self, rucio_client):
                       'domains': {
                           'wan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'}]
         for p in protocols:
             rucio_client.add_protocol(protocol_rse, p)
@@ -1202,7 +1202,7 @@ def test_update_protocols_various_attributes(self, vo, rucio_client):
                       'domains': {
                           'lan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'}]
         for p in protocols:
             rucio_client.add_protocol(protocol_rse, p)
@@ -1227,7 +1227,7 @@ def test_swap_protocol(self, rucio_client):
                       'domains': {
                           'lan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCKB',
                       'hostname': 'localhost',
@@ -1237,7 +1237,7 @@ def test_swap_protocol(self, rucio_client):
                       'domains': {
                           'lan': {'read': 2,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCKC',
                       'hostname': 'localhost',
@@ -1246,7 +1246,7 @@ def test_swap_protocol(self, rucio_client):
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
                           'lan': {'read': 3,
-                                  'write': 0,
+                                  'write': None,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
         for p in protocols:
@@ -1284,7 +1284,7 @@ def test_update_protocols_not_supported(self, rucio_client):
                       'domains': {
                           'lan': {'read': 1,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK',
                       'hostname': 'localhost',
@@ -1294,7 +1294,7 @@ def test_update_protocols_not_supported(self, rucio_client):
                       'domains': {
                           'lan': {'read': 0,
                                   'write': 1,
-                                  'delete': 0}},
+                                  'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'},
                      {'scheme': 'MOCK_DELETE',
                       'hostname': 'localhost',
@@ -1302,8 +1302,8 @@ def test_update_protocols_not_supported(self, rucio_client):
                       'prefix': '/the/one/with/all/the/files',
                       'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {
-                          'lan': {'read': 0,
-                                  'write': 0,
+                          'lan': {'read': None,
+                                  'write': None,
                                   'delete': 1}},
                       'extended_attributes': 'TheOneWithAllTheRest'}, ]
         for p in protocols:
@@ -1326,7 +1326,7 @@ def test_update_protocols_invalid_value(self, rucio_client):
                       # 'impl': 'rucio.rse.protocols.SomeProtocol.SomeImplementation',
                       'domains': {'lan': {'read': 1,
                                           'write': 1,
-                                          'delete': 0}},
+                                          'delete': None}},
                       'extended_attributes': 'TheOneWithAllTheRest'}
 
         try:
@@ -1357,7 +1357,7 @@ def test_update_protocol_wrong_priority(self, rucio_client):
                                        'extended_attributes': 'TheOneWithAllTheRest'})
         try:
             with pytest.raises(exception.RSEProtocolPriorityError):
-                rucio_client.update_protocols(protocol_rse, scheme='MOCK', hostname='localhost', port=42, data={'domains': {'lan': {'read': 4}}})
+                rucio_client.update_protocols(protocol_rse, scheme='MOCK', hostname='localhost', port=42, data={'domains': {'lan': {'read': -1}}})
         finally:
             rucio_client.delete_rse(protocol_rse)
 
@@ -1421,23 +1421,24 @@ def test_rsemgr_possible_protocols(self):
                         'credentials': None,
                         'deterministic': True,
                         'domain': ['lan', 'wan'],
-                        'protocols': [{'domains': {'lan': {'delete': 2, 'read': 0, 'write': 0},
-                                                   'wan': {'delete': 2, 'read': 2, 'write': 0}},
+                        'protocols': [{'domains': {'lan': {'delete': 2, 'read': None, 'write': None},
+                                                   'wan': {'delete': 2, 'read': 2, 'write': None}},
                                        'extended_attributes': None,
                                        'hostname': 'atlas-xrd.gridpp.rl.ac.uk',
                                        'impl': 'rucio.rse.protocols.gfal.Default',
                                        'port': 1094,
                                        'prefix': '//castor/ads.rl.ac.uk/prod/atlas/stripInput/atlasdatadisk/rucio/',
                                        'scheme': 'root'},
-                                      {'domains': {'lan': {'delete': 0, 'read': 1, 'write': 0},
-                                                   'wan': {'delete': 0, 'read': 0, 'write': 0}},
+                                      {'domains': {'lan': {'delete': None, 'read': 1, 'write': None},
+                                                   'wan': {'delete': None, 'read': None, 'write': None}},
+
                                        'extended_attributes': None,
                                        'hostname': 'catlasdlf.ads.rl.ac.uk',
                                        'impl': 'rucio.rse.protocols.gfal.Default',
                                        'port': 1094,
                                        'prefix': '//castor/ads.rl.ac.uk/prod/atlas/stripInput/atlasdatadisk/rucio/',
                                        'scheme': 'root'},
-                                      {'domains': {'lan': {'delete': 1, 'read': 0, 'write': 1},
+                                      {'domains': {'lan': {'delete': 1, 'read': None, 'write': 1},
                                                    'wan': {'delete': 1, 'read': 1, 'write': 1}},
                                        'extended_attributes': {'space_token': 'ATLASDATADISK',
                                                                'web_service_path': '/srm/managerv2?SFN='},
diff --git a/lib/rucio/tests/test_upload.py b/lib/rucio/tests/test_upload.py
--- a/lib/rucio/tests/test_upload.py
+++ b/lib/rucio/tests/test_upload.py
@@ -195,7 +195,7 @@ def test_multiple_protocols_same_scheme(rse_factory, upload_client, mock_scope,
                           'prefix': '/prefix1/',
                           'impl': 'rucio.rse.protocols.posix.Default',
                           'domains': {
-                              'lan': {'read': 0, 'write': 0, 'delete': 0},
+                              'lan': {'read': None, 'write': None, 'delete': None},
                               'wan': {'read': 1, 'write': 1, 'delete': 1}}})
     add_protocol(rse_id, {'scheme': 'file',
                           'hostname': 'file-lan.aperture.com',
@@ -204,7 +204,7 @@ def test_multiple_protocols_same_scheme(rse_factory, upload_client, mock_scope,
                           'impl': 'rucio.rse.protocols.posix.Default',
                           'domains': {
                               'lan': {'read': 1, 'write': 1, 'delete': 1},
-                              'wan': {'read': 0, 'write': 0, 'delete': 0}}})
+                              'wan': {'read': None, 'write': None, 'delete': None}}})
     add_protocol(rse_id, {'scheme': 'root',
                           'hostname': 'root.aperture.com',
                           'port': 1403,