Skip to content

Commit 66c54e3

Browse files
committed
RDBC-1059 Add Disabled flag to CertificateMetadata
1 parent 3a8f18a commit 66c54e3

3 files changed

Lines changed: 220 additions & 3 deletions

File tree

ravendb/serverwide/operations/certificates.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(
5656
collection_primary_key: str = None,
5757
public_key_pinning_hash: str = None,
5858
not_before: datetime = None,
59+
disabled: bool = False,
5960
):
6061
self.name = name
6162
self.security_clearance = security_clearance
@@ -66,6 +67,7 @@ def __init__(
6667
self.collection_secondary_keys = collection_secondary_keys
6768
self.public_key_pinning_hash = public_key_pinning_hash
6869
self.not_before = not_before
70+
self.disabled = disabled
6971

7072
@classmethod
7173
def from_json(cls, json_dict: dict) -> CertificateMetadata:
@@ -79,6 +81,7 @@ def from_json(cls, json_dict: dict) -> CertificateMetadata:
7981
json_dict.get("CollectionPrimaryKey", None),
8082
json_dict.get("PublicKeyPinningHash", None),
8183
Utils.string_to_datetime(json_dict["NotBefore"]) if "NotBefore" in json_dict else None,
84+
json_dict.get("Disabled", False),
8285
)
8386

8487

@@ -95,6 +98,7 @@ def __init__(
9598
collection_secondary_keys: List[str] = None,
9699
collection_primary_key: str = None,
97100
public_key_pinning_hash: str = None,
101+
disabled: bool = False,
98102
):
99103
super().__init__(
100104
name,
@@ -105,6 +109,7 @@ def __init__(
105109
collection_secondary_keys,
106110
collection_primary_key,
107111
public_key_pinning_hash,
112+
disabled=disabled,
108113
)
109114
self.certificate = certificate
110115
self.password = password
@@ -120,6 +125,7 @@ def to_json(self) -> dict:
120125
"PublicKeyPinningHash": self.public_key_pinning_hash,
121126
"Certificate": self.certificate,
122127
"Password": self.password,
128+
"Disabled": self.disabled,
123129
}
124130
if self.not_after:
125131
json_dict.update({"NotAfter": Utils.datetime_to_string(self.not_after)})
@@ -138,6 +144,7 @@ def from_json(cls, json_dict: dict) -> CertificateDefinition:
138144
json_dict["CollectionSecondaryKeys"],
139145
json_dict["CollectionPrimaryKey"],
140146
json_dict["PublicKeyPinningHash"],
147+
disabled=json_dict.get("Disabled", False),
141148
)
142149

143150

@@ -443,12 +450,18 @@ def get_raft_unique_request_id(self) -> str:
443450
class EditClientCertificateOperation(VoidServerOperation):
444451
class Parameters:
445452
def __init__(
446-
self, thumbprint: str, permissions: Dict[str, DatabaseAccess], name: str, clearance: SecurityClearance
453+
self,
454+
thumbprint: str,
455+
permissions: Dict[str, DatabaseAccess],
456+
name: str,
457+
clearance: SecurityClearance,
458+
disabled: bool = False,
447459
):
448460
self.thumbprint = thumbprint
449461
self.permissions = permissions
450462
self.name = name
451463
self.clearance = clearance
464+
self.disabled = disabled
452465

453466
def __init__(self, parameters: Parameters):
454467
if parameters is None:
@@ -467,19 +480,28 @@ def __init__(self, parameters: Parameters):
467480
self.__thumbprint = parameters.thumbprint
468481
self.__permissions = parameters.permissions
469482
self.__clearance = parameters.clearance
483+
self.__disabled = parameters.disabled
470484

471485
def get_command(self, conventions: "DocumentConventions") -> "VoidRavenCommand":
472-
return self.__EditCertificateClientCommand(self.__thumbprint, self.__name, self.__permissions, self.__clearance)
486+
return self.__EditCertificateClientCommand(
487+
self.__thumbprint, self.__name, self.__permissions, self.__clearance, self.__disabled
488+
)
473489

474490
class __EditCertificateClientCommand(VoidRavenCommand, RaftCommand):
475491
def __init__(
476-
self, thumbprint: str, name: str, permissions: Dict[str, DatabaseAccess], clearance: SecurityClearance
492+
self,
493+
thumbprint: str,
494+
name: str,
495+
permissions: Dict[str, DatabaseAccess],
496+
clearance: SecurityClearance,
497+
disabled: bool,
477498
):
478499
super().__init__()
479500
self.__thumbprint = thumbprint
480501
self.__name = name
481502
self.__permissions = permissions
482503
self.__clearance = clearance
504+
self.__disabled = disabled
483505

484506
def is_read_request(self) -> bool:
485507
return False
@@ -492,6 +514,7 @@ def create_request(self, node: ServerNode) -> requests.Request:
492514
definition.permissions = self.__permissions
493515
definition.security_clearance = self.__clearance
494516
definition.name = self.__name
517+
definition.disabled = self.__disabled
495518

496519
request = requests.Request("POST", url)
497520
request.data = definition.to_json()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Integration tests against a live RavenDB 7.2.x HTTPS server for the
3+
certificate `disabled` flag added in 7.2.3.
4+
5+
Verifies that the flag round-trips through:
6+
* EditClientCertificateOperation outbound (disabled in `Parameters`)
7+
* GetCertificatesOperation / GetCertificateOperation inbound (Disabled in
8+
JSON → CertificateDefinition.disabled)
9+
* GetCertificateMetadataOperation inbound (Disabled in JSON →
10+
CertificateMetadata.disabled)
11+
"""
12+
13+
import unittest
14+
15+
from ravendb.serverwide.operations.certificates import (
16+
CreateClientCertificateOperation,
17+
DatabaseAccess,
18+
EditClientCertificateOperation,
19+
GetCertificateMetadataOperation,
20+
GetCertificateOperation,
21+
GetCertificatesOperation,
22+
SecurityClearance,
23+
)
24+
from ravendb.tests.test_base import TestBase
25+
26+
27+
class TestCertificateDisabledFlagIntegration(TestBase):
28+
def test_edit_can_disable_certificate(self):
29+
with self.secured_document_store as store:
30+
# Create a fresh client certificate to disable.
31+
create_op = CreateClientCertificateOperation(
32+
"test-disable-cert",
33+
{"test_db": DatabaseAccess.READ_WRITE},
34+
SecurityClearance.VALID_USER,
35+
)
36+
store.maintenance.server.send(create_op)
37+
38+
# Find its thumbprint from the listing.
39+
all_certs = store.maintenance.server.send(GetCertificatesOperation(0, 200))
40+
mine = next(c for c in all_certs if c.name == "test-disable-cert")
41+
self.assertFalse(mine.disabled) # baseline
42+
43+
# Disable via EditClientCertificateOperation.
44+
store.maintenance.server.send(
45+
EditClientCertificateOperation(
46+
EditClientCertificateOperation.Parameters(
47+
thumbprint=mine.thumbprint,
48+
permissions={"test_db": DatabaseAccess.READ_WRITE},
49+
name="test-disable-cert",
50+
clearance=SecurityClearance.VALID_USER,
51+
disabled=True,
52+
)
53+
)
54+
)
55+
56+
# Read back via GetCertificateOperation; Disabled must round-trip.
57+
single = store.maintenance.server.send(GetCertificateOperation(mine.thumbprint))
58+
self.assertIsNotNone(single)
59+
self.assertTrue(single.disabled)
60+
61+
# Read back via GetCertificatesOperation listing too.
62+
all_certs = store.maintenance.server.send(GetCertificatesOperation(0, 200))
63+
mine_after = next(c for c in all_certs if c.thumbprint == mine.thumbprint)
64+
self.assertTrue(mine_after.disabled)
65+
66+
# Read back via GetCertificateMetadataOperation (uses
67+
# CertificateMetadata.from_json, the other deserialization path).
68+
metadata = store.maintenance.server.send(GetCertificateMetadataOperation(mine.thumbprint))
69+
self.assertIsNotNone(metadata)
70+
self.assertTrue(metadata.disabled)
71+
72+
def test_edit_can_re_enable_certificate(self):
73+
with self.secured_document_store as store:
74+
create_op = CreateClientCertificateOperation(
75+
"test-reenable-cert",
76+
{"test_db": DatabaseAccess.READ},
77+
SecurityClearance.VALID_USER,
78+
)
79+
store.maintenance.server.send(create_op)
80+
81+
certs = store.maintenance.server.send(GetCertificatesOperation(0, 200))
82+
mine = next(c for c in certs if c.name == "test-reenable-cert")
83+
84+
# Disable, then re-enable.
85+
for desired_state in (True, False):
86+
store.maintenance.server.send(
87+
EditClientCertificateOperation(
88+
EditClientCertificateOperation.Parameters(
89+
thumbprint=mine.thumbprint,
90+
permissions={"test_db": DatabaseAccess.READ},
91+
name="test-reenable-cert",
92+
clearance=SecurityClearance.VALID_USER,
93+
disabled=desired_state,
94+
)
95+
)
96+
)
97+
round_tripped = store.maintenance.server.send(GetCertificateOperation(mine.thumbprint))
98+
self.assertEqual(desired_state, round_tripped.disabled)
99+
100+
101+
if __name__ == "__main__":
102+
unittest.main()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
Unit tests for the 7.2.3 certificate `disabled` flag added to
3+
CertificateMetadata and EditClientCertificateOperation.Parameters.
4+
"""
5+
6+
import unittest
7+
8+
from ravendb.serverwide.operations.certificates import (
9+
CertificateDefinition,
10+
CertificateMetadata,
11+
DatabaseAccess,
12+
EditClientCertificateOperation,
13+
SecurityClearance,
14+
)
15+
16+
17+
class TestCertificateDisabledFlag(unittest.TestCase):
18+
def test_metadata_default_is_false(self):
19+
meta = CertificateMetadata()
20+
self.assertFalse(meta.disabled)
21+
22+
def test_metadata_round_trip_through_from_json(self):
23+
meta = CertificateMetadata.from_json(
24+
{
25+
"Name": "n",
26+
"SecurityClearance": "ValidUser",
27+
"Disabled": True,
28+
}
29+
)
30+
self.assertTrue(meta.disabled)
31+
32+
def test_definition_includes_disabled_in_to_json(self):
33+
d = CertificateDefinition()
34+
d.disabled = True
35+
self.assertTrue(d.to_json()["Disabled"])
36+
37+
def test_definition_init_accepts_disabled(self):
38+
d = CertificateDefinition(disabled=True)
39+
self.assertTrue(d.disabled)
40+
41+
def test_definition_round_trips_disabled(self):
42+
# Round-trip: deserialize a server-style payload, verify the flag survives.
43+
payload = {
44+
"Certificate": "c",
45+
"Password": None,
46+
"Name": "n",
47+
"SecurityClearance": "ValidUser",
48+
"Thumbprint": "tp",
49+
"NotAfter": None,
50+
"Permissions": {},
51+
"CollectionSecondaryKeys": [],
52+
"CollectionPrimaryKey": "",
53+
"PublicKeyPinningHash": None,
54+
"Disabled": True,
55+
}
56+
d = CertificateDefinition.from_json(payload)
57+
self.assertTrue(d.disabled)
58+
59+
def test_definition_from_json_disabled_defaults_false(self):
60+
# Older servers won't emit the field at all; we should default to False.
61+
payload = {
62+
"Certificate": "c",
63+
"Password": None,
64+
"Name": "n",
65+
"SecurityClearance": "ValidUser",
66+
"Thumbprint": "tp",
67+
"NotAfter": None,
68+
"Permissions": {},
69+
"CollectionSecondaryKeys": [],
70+
"CollectionPrimaryKey": "",
71+
"PublicKeyPinningHash": None,
72+
}
73+
d = CertificateDefinition.from_json(payload)
74+
self.assertFalse(d.disabled)
75+
76+
def test_edit_operation_parameters_carries_disabled(self):
77+
params = EditClientCertificateOperation.Parameters(
78+
thumbprint="abc",
79+
permissions={"db1": DatabaseAccess.READ},
80+
name="my-cert",
81+
clearance=SecurityClearance.VALID_USER,
82+
disabled=True,
83+
)
84+
op = EditClientCertificateOperation(params)
85+
# The disabled flag flows through to the command and into the request body
86+
# via the definition's to_json. Verify by reaching through the private
87+
# field on the operation instance.
88+
self.assertTrue(op._EditClientCertificateOperation__disabled)
89+
90+
91+
if __name__ == "__main__":
92+
unittest.main()

0 commit comments

Comments
 (0)