Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* The updated `config_schema.json` will cause a validation error if you have the developer_settings configured with no items uncommented below it. To resolve this comment out developer_settings in your config.yaml (using #) if you do not have any developer settings configured. ([#4715](https://github.com/microsoft/AzureTRE/pull/4715))

ENHANCEMENTS:
* Add automatic certificate renewal capability to certificate shared service
* Create CODEOWNERS file with repository maintainers
* Change Guacamole VM OS disk defaults to Standard SSD ([#4621](https://github.com/microsoft/AzureTRE/issues/4621))
* Add additional Databricks, Microsoft & Python domains to allowed-dns.json ([#4636](https://github.com/microsoft/AzureTRE/pull/4636))
Expand All @@ -31,6 +32,9 @@ BUG FIXES:
* Updated config_schema.json to include missing values. ([#4712](https://github.com/microsoft/AzureTRE/issues/4712))([#4714](https://github.com/microsoft/AzureTRE/issues/4714))
* Remove workspace upgrade step from databricks template ([#4726](https://github.com/microsoft/AzureTRE/pull/4726))

COMPONENTS:
* Certificate shared service `tre-shared-service-certs` to version 0.8.0

## 0.25.0 (July 18, 2025)
**IMPORTANT**:
* If you update core deployment prior to this release an upstream issue with Azure RM terraform provider means that
Expand Down
16 changes: 16 additions & 0 deletions api_app/tests_ma/test_api/test_routes/test_shared_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ def shared_service_input():
}


@pytest.fixture
def certs_service_input():
return {
"templateName": "tre-shared-service-certs",
"properties": {
"display_name": "Certificate Service",
"description": "SSL certificate service with auto-renewal",
"domain_prefix": "test",
"cert_name": "test-cert",
"enable_auto_renewal": True,
"renewal_threshold_days": 30,
"renewal_schedule_cron": "0 2 * * 0"
}
}


def sample_shared_service(shared_service_id=SHARED_SERVICE_ID):
return SharedService(
id=shared_service_id,
Expand Down
203 changes: 203 additions & 0 deletions api_app/tests_ma/test_services/test_cert_auto_renewal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import pytest
import json
from unittest.mock import patch, MagicMock

from jsonschema import validate, ValidationError
from services.schema_service import enrich_template


class TestCertAutoRenewal:
"""Test certificate auto-renewal functionality."""

@pytest.fixture
def cert_template_schema(self):
"""Sample certificate template schema with auto-renewal parameters."""
return {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://github.com/microsoft/AzureTRE/templates/shared_services/certs/template_schema.json",
"type": "object",
"title": "Certificate Service",
"description": "Provides SSL Certs for a specified internal domain",
"required": [
"domain_prefix",
"cert_name"
],
"properties": {
"display_name": {
"type": "string",
"title": "Name for the workspace service",
"description": "The name of the workspace service to be displayed to users",
"default": "Certificate Service",
"updateable": True
},
"domain_prefix": {
"type": "string",
"title": "Domain prefix",
"description": "The FQDN prefix to generate a certificate for"
},
"cert_name": {
"type": "string",
"title": "Cert name",
"description": "What to call the certificate exported to KeyVault"
},
"enable_auto_renewal": {
"type": "boolean",
"title": "Enable Auto-renewal",
"description": "Enable automatic renewal of the certificate before expiry",
"default": False,
"updateable": True
},
"renewal_threshold_days": {
"type": "integer",
"title": "Renewal threshold (days)",
"description": "Number of days before expiry to trigger renewal",
"default": 30,
"minimum": 1,
"maximum": 60,
"updateable": True
},
"renewal_schedule_cron": {
"type": "string",
"title": "Renewal schedule (cron)",
"description": "Cron expression for checking certificate expiry",
"default": "0 2 * * 0",
"updateable": True
}
}
}

def test_auto_renewal_schema_validation_success(self, cert_template_schema):
"""Test that valid auto-renewal parameters pass schema validation."""
valid_payload = {
"domain_prefix": "nexus",
"cert_name": "nexus-ssl",
"enable_auto_renewal": True,
"renewal_threshold_days": 30,
"renewal_schedule_cron": "0 2 * * 0"
}

# Should not raise ValidationError
validate(instance=valid_payload, schema=cert_template_schema)

def test_auto_renewal_schema_validation_with_defaults(self, cert_template_schema):
"""Test that minimal payload with defaults works."""
minimal_payload = {
"domain_prefix": "test",
"cert_name": "test-cert"
}

# Should not raise ValidationError
validate(instance=minimal_payload, schema=cert_template_schema)

def test_auto_renewal_threshold_validation(self, cert_template_schema):
"""Test that renewal threshold validation works."""
# Test invalid threshold - too low
with pytest.raises(ValidationError):
invalid_payload = {
"domain_prefix": "test",
"cert_name": "test-cert",
"renewal_threshold_days": 0
}
validate(instance=invalid_payload, schema=cert_template_schema)

# Test invalid threshold - too high
with pytest.raises(ValidationError):
invalid_payload = {
"domain_prefix": "test",
"cert_name": "test-cert",
"renewal_threshold_days": 100
}
validate(instance=invalid_payload, schema=cert_template_schema)

# Test valid thresholds
for valid_threshold in [1, 15, 30, 45, 60]:
valid_payload = {
"domain_prefix": "test",
"cert_name": "test-cert",
"renewal_threshold_days": valid_threshold
}
validate(instance=valid_payload, schema=cert_template_schema)

def test_auto_renewal_updateable_fields(self, cert_template_schema):
"""Test that auto-renewal fields are marked as updateable."""
properties = cert_template_schema["properties"]

updateable_fields = [
"enable_auto_renewal",
"renewal_threshold_days",
"renewal_schedule_cron"
]

for field in updateable_fields:
assert properties[field].get("updateable", False) is True, \
f"Field {field} should be updateable"

def test_auto_renewal_default_values(self, cert_template_schema):
"""Test that auto-renewal fields have correct default values."""
properties = cert_template_schema["properties"]

expected_defaults = {
"enable_auto_renewal": False,
"renewal_threshold_days": 30,
"renewal_schedule_cron": "0 2 * * 0"
}

for field, expected_default in expected_defaults.items():
actual_default = properties[field].get("default")
assert actual_default == expected_default, \
f"Field {field} should have default value {expected_default}, got {actual_default}"

def test_missing_required_fields(self, cert_template_schema):
"""Test that missing required fields fail validation."""
# Missing domain_prefix
with pytest.raises(ValidationError):
validate(instance={"cert_name": "test"}, schema=cert_template_schema)

# Missing cert_name
with pytest.raises(ValidationError):
validate(instance={"domain_prefix": "test"}, schema=cert_template_schema)

@pytest.mark.parametrize("auto_renewal_enabled,threshold,cron", [
(True, 7, "0 1 * * *"), # Daily at 1 AM
(True, 14, "0 2 * * 1"), # Weekly on Monday at 2 AM
(True, 45, "0 3 1 * *"), # Monthly on 1st at 3 AM
(False, 30, "0 2 * * 0"), # Disabled with defaults
])
def test_auto_renewal_parameter_combinations(self, cert_template_schema, auto_renewal_enabled, threshold, cron):
"""Test various combinations of auto-renewal parameters."""
payload = {
"domain_prefix": "test",
"cert_name": "test-cert",
"enable_auto_renewal": auto_renewal_enabled,
"renewal_threshold_days": threshold,
"renewal_schedule_cron": cron
}

# Should not raise ValidationError
validate(instance=payload, schema=cert_template_schema)

def test_type_validation(self, cert_template_schema):
"""Test that incorrect types fail validation."""
# Wrong type for enable_auto_renewal
with pytest.raises(ValidationError):
validate(instance={
"domain_prefix": "test",
"cert_name": "test-cert",
"enable_auto_renewal": "true" # Should be boolean
}, schema=cert_template_schema)

# Wrong type for renewal_threshold_days
with pytest.raises(ValidationError):
validate(instance={
"domain_prefix": "test",
"cert_name": "test-cert",
"renewal_threshold_days": "30" # Should be integer
}, schema=cert_template_schema)

# Wrong type for renewal_schedule_cron
with pytest.raises(ValidationError):
validate(instance={
"domain_prefix": "test",
"cert_name": "test-cert",
"renewal_schedule_cron": 123 # Should be string
}, schema=cert_template_schema)
Loading
Loading