|
1 | 1 | import hashlib |
| 2 | +import hmac |
2 | 3 | import json |
3 | 4 | import logging |
4 | 5 | import os |
@@ -556,6 +557,77 @@ class Meta: |
556 | 557 | ) |
557 | 558 |
|
558 | 559 |
|
| 560 | +class EnvVarHeaderContentGuard(ContentGuard, AutoAddObjPermsMixin): |
| 561 | + """ |
| 562 | + Content guard that validates a Base64-encoded header against a server-side environment variable. |
| 563 | +
|
| 564 | + Clients and proxies must send the expected secret as a Base64-encoded UTF-8 string in |
| 565 | + ``header_name``. Pulp decodes the header, then compares the result to the value of |
| 566 | + ``os.environ[env_var]`` using a timing-safe comparison. |
| 567 | +
|
| 568 | + ``env_var`` must be listed in ``settings.ENVVAR_HEADER_CONTENT_GUARD_ALLOWED_VARS``. |
| 569 | + The expected secret is read from the content-app process environment at request time |
| 570 | + so rotation only requires updating the environment and redeploying. |
| 571 | + """ |
| 572 | + |
| 573 | + TYPE = "envvar_header" |
| 574 | + |
| 575 | + header_name = models.TextField() |
| 576 | + env_var = models.TextField() |
| 577 | + |
| 578 | + def permit(self, request): |
| 579 | + if self.env_var not in settings.ENVVAR_HEADER_CONTENT_GUARD_ALLOWED_VARS: |
| 580 | + _logger.debug( |
| 581 | + "Access not allowed. Environment variable %s is not in " |
| 582 | + "ENVVAR_HEADER_CONTENT_GUARD_ALLOWED_VARS.", |
| 583 | + self.env_var, |
| 584 | + ) |
| 585 | + raise PermissionError(_("Access denied.")) |
| 586 | + |
| 587 | + header_content = request.headers.get(self.header_name) |
| 588 | + if not header_content: |
| 589 | + _logger.debug("Access not allowed. Header %s not found.", self.header_name) |
| 590 | + raise PermissionError(_("Access denied.")) |
| 591 | + |
| 592 | + try: |
| 593 | + header_decoded_content = b64decode(header_content, validate=True) |
| 594 | + except Base64DecodeError: |
| 595 | + _logger.debug("Access not allowed - Header content is not Base64 encoded.") |
| 596 | + raise PermissionError(_("Access denied.")) from None |
| 597 | + |
| 598 | + try: |
| 599 | + header_value = header_decoded_content.decode("utf-8") |
| 600 | + except UnicodeDecodeError: |
| 601 | + _logger.debug("Access not allowed - Header content is not valid UTF-8.") |
| 602 | + raise PermissionError(_("Access denied.")) from None |
| 603 | + |
| 604 | + expected = os.environ.get(self.env_var) |
| 605 | + if expected is None or expected.rstrip("\r\n") == "": |
| 606 | + _logger.warning( |
| 607 | + "Access not allowed. Environment variable %s is unset or empty.", self.env_var |
| 608 | + ) |
| 609 | + raise PermissionError(_("Access denied.")) |
| 610 | + |
| 611 | + expected_stripped = expected.rstrip("\r\n") |
| 612 | + if not hmac.compare_digest( |
| 613 | + header_value.encode("utf-8"), |
| 614 | + expected_stripped.encode("utf-8"), |
| 615 | + ): |
| 616 | + _logger.debug("Access not allowed. Header value does not match environment variable.") |
| 617 | + raise PermissionError(_("Access denied.")) |
| 618 | + |
| 619 | + return |
| 620 | + |
| 621 | + class Meta: |
| 622 | + default_related_name = "%(app_label)s_%(model_name)s" |
| 623 | + permissions = ( |
| 624 | + ( |
| 625 | + "manage_roles_envvarheadercontentguard", |
| 626 | + "Can manage role assignments on EnvVar Header content guard", |
| 627 | + ), |
| 628 | + ) |
| 629 | + |
| 630 | + |
559 | 631 | class CompositeContentGuard(ContentGuard, AutoAddObjPermsMixin): |
560 | 632 | """ |
561 | 633 | Content guard to allow a list of contentguards to be evaluated on access. |
|
0 commit comments