Skip to content
Merged
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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Change Log (v2.8.1+)

## v4.4.0 [2025-10-24]

__What's New:__

* Added Manager Approval support to `[application_management|secrets_manager|system]`.
* Added GCP Federation Provider.

__Enhancements:__

* Added `manager_condition` parameter to `[application_management.profiles|secrets_manager|system].policies.build`.
* Drop `socket` usage to speed up response times in specific scenarios, e.g., Windows DNS in WSL environments.

__Bug Fixes:__

* None

__Dependencies:__

* None

__Other:__

* Test naming convention updates.

## v4.3.2 [2025-09-04]

__What's New:__
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ keywords = ["britive", "cpam", "identity", "jit"]

[project.optional-dependencies]
azure = ["azure-identity"]
gcp = ["google-auth"]

[project.urls]
Homepage = "https://www.britive.com"
Expand Down
2 changes: 1 addition & 1 deletion src/britive/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '4.3.2'
__version__ = '4.4.0'
4 changes: 3 additions & 1 deletion src/britive/application_management/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ def scan(self, application_id: str, org_scan_only: bool = False) -> dict:
:return: Details of the scan that was initiated.
"""

return self.britive.application_management.scans.scan(application_id=application_id, org_scan_only=org_scan_only)
return self.britive.application_management.scans.scan(
application_id=application_id, org_scan_only=org_scan_only
)

def delete(self, application_id: str) -> None:
"""
Expand Down
8 changes: 7 additions & 1 deletion src/britive/application_management/profiles/policies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Union
from typing import Literal, Union


class Policies:
Expand All @@ -25,6 +25,7 @@ def build( # noqa: PLR0913
access_validity_time: int = 120,
approver_users: list = None,
approver_tags: list = None,
manager_condition: Literal['All', 'Any', 'Manager'] = '',
access_type: str = 'Allow',
identifier_type: str = 'name',
condition_as_dict: bool = False,
Expand Down Expand Up @@ -73,6 +74,10 @@ def build( # noqa: PLR0913
If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required.
:param approver_tags: Optional list of tag names who are considered approvers.
If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required.
:param manager_condition: Optional condition to enable requiring user's manager approval. Valid values are
`Any` or `All` or `Manager`. `Any` corresponds to manager approval required, `All` corresponds to
manager and approver_users/approver_tags approval required, and `Manager` corresponds to just the manager's
approval required
:param access_type: The type of access this policy provides. Valid values are `Allow` and `Deny`. Defaults
to `Allow`.
:param identifier_type: Valid values are `id` or `name`. Defaults to `name`. Represents which type of
Expand Down Expand Up @@ -105,6 +110,7 @@ def build( # noqa: PLR0913
access_validity_time=access_validity_time,
approver_users=approver_users,
approver_tags=approver_tags,
manager_condition=manager_condition,
access_type=access_type,
identifier_type=identifier_type,
condition_as_dict=condition_as_dict,
Expand Down
4 changes: 4 additions & 0 deletions src/britive/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class MethodNotAllowed(BritiveException):
class MissingAzureDependency(BritiveException):
pass

class MissingGcpDependency(BritiveException):
pass

class NoSecretsVaultFound(BritiveException):
pass
Expand All @@ -57,6 +59,8 @@ class NoSecretsVaultFound(BritiveException):
class NotExecutingInAzureEnvironment(BritiveException):
pass

class NotExecutingInGcpEnvironment(BritiveException):
pass

class NotExecutingInBitbucketEnvironment(BritiveException):
pass
Expand Down
2 changes: 2 additions & 0 deletions src/britive/federation_providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .azure_user_assigned_managed_identity import AzureUserAssignedManagedIdentityFederationProvider
from .bitbucket import BitbucketFederationProvider
from .federation_provider import FederationProvider
from .gcp import GcpFederationProvider
from .github import GithubFederationProvider
from .gitlab import GitlabFederationProvider
from .spacelift import SpaceliftFederationProvider
Expand All @@ -14,6 +15,7 @@ def __init__(self, britive) -> None:
self.azure_system_assigned_managed_identity = AzureSystemAssignedManagedIdentityFederationProvider(britive)
self.azure_user_assigned_managed_identity = AzureUserAssignedManagedIdentityFederationProvider(britive)
self.bitbucket = BitbucketFederationProvider(britive)
self.gcp = GcpFederationProvider(britive)
self.generic = FederationProvider(britive)
self.github = GithubFederationProvider(britive)
self.gitlab = GitlabFederationProvider(britive)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def get_token(self) -> str:
return f'OIDC::{token}'
except ImportError as e:
raise MissingAzureDependency(
'`azure-identity` package required to use the azure managed identity federation provider'
'azure dependency package required to use the azure managed identity federation provider, '
'install with `pip install britive[azure]'
) from e
except CredentialUnavailableError as e:
msg = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def get_token(self) -> str:
return f'OIDC::{token}'
except ImportError as e:
raise MissingAzureDependency(
'`azure-identity` package required to use the azure managed identity federation provider'
'azure dependency package required to use the azure managed identity federation provider, '
'install with `pip install britive[azure]'
) from e
except CredentialUnavailableError as e:
msg = (
Expand Down
30 changes: 30 additions & 0 deletions src/britive/federation_providers/gcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from britive.exceptions import MissingGcpDependency, NotExecutingInGcpEnvironment

from .federation_provider import FederationProvider


class GcpFederationProvider(FederationProvider):
def __init__(self, audience: str = None) -> None:
self.audience = audience if audience else 'https://accounts.google.com/'
super().__init__()

def get_token(self):
try:
from google.auth.exceptions import DefaultCredentialsError
from google.auth.transport.requests import Request
from google.oauth2 import id_token

token = id_token.fetch_id_token(Request(), self.audience)

return f'OIDC::{token}'
except ImportError as e:
raise MissingGcpDependency(
'google dependency package required to use the gcp managed identity federation provider, '
'install with `pip install britive[gcp]'
) from e
except DefaultCredentialsError as e:
msg = (
'the codebase is not executing in an Gcp environment or some other issue is causing the '
'managed identity credentials to be unavailable'
)
raise NotExecutingInGcpEnvironment(msg) from e
30 changes: 19 additions & 11 deletions src/britive/helpers/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import socket
import warnings
from typing import Optional, Union

import requests
import urllib3

from britive.exceptions import BritiveException, InvalidFederationProvider, allowed_exceptions
from britive.exceptions.badrequest import bad_request_code_map
Expand All @@ -12,6 +13,7 @@
AzureSystemAssignedManagedIdentityFederationProvider,
AzureUserAssignedManagedIdentityFederationProvider,
BitbucketFederationProvider,
GcpFederationProvider,
GithubFederationProvider,
GitlabFederationProvider,
SpaceliftFederationProvider,
Expand Down Expand Up @@ -59,18 +61,20 @@
return 'none'


def parse_tenant(tenant: str) -> str:
domain = tenant.replace('https://', '').replace('http://', '').split('/')[0] # remove scheme and paths
def parse_tenant(tenant: str, timeout: float = 3) -> str:
if not (domain := urllib3.util.parse_url(tenant).host).endswith('britive-app.com'):
domain = f'{domain}.britive-app.com'
try:
socket.getaddrinfo(host=domain, port=443) # if success then a full domain was provided
requests.head(f'https://{domain}/api/health', timeout=timeout)
return domain
except socket.gaierror: # assume just the tenant name was provided (originally the only supported method)
resolved_domain = f'{tenant}.britive-app.com'
try:
socket.getaddrinfo(host=resolved_domain, port=443) # validate the hostname is real
return resolved_domain # and if so set the tenant accordingly
except socket.gaierror as e:
raise InvalidTenantError(f'Invalid tenant provided: {tenant}. DNS resolution failed.') from e
except requests.exceptions.Timeout:
original = warnings.formatwarning
warnings.formatwarning = lambda msg, *a, **k: f'{msg}\n'
warnings.warn(f'WARNING: Tenant validation timed out, but domain structure is valid: [{domain}]')
warnings.formatwarning = original
return domain
except requests.exceptions.ConnectionError as e:
raise InvalidTenantError(f'Invalid tenant provided: {tenant}. Domain resolution failed.') from e


def response_has_no_content(response) -> bool:
Expand Down Expand Up @@ -128,6 +132,9 @@
`azuresmi-<audience>` and `azureumi-<client-id>|<audience>`. If no audience is provided the default audience
of `https://management.azure.com/` will be used.
For the GCP provider it is possible to provide an OIDC audience value via
`gcp-<audience>`. If no audience is provided the default audience of https://accounts.google.com/ will be used.
For the Github provider it is possible to provide an OIDC audience value via `github-<audience>`. If no
audience is provided the default Github audience value will be used.
Expand All @@ -151,6 +158,7 @@
profile=safe_list_get(helper, 1), tenant=tenant, duration=duration_seconds
).get_token(),
'bitbucket': lambda: BitbucketFederationProvider().get_token(),
'gcp': lambda: GcpFederationProvider().get_token(audience=safe_list_get(helper, 1)).get_token(),
'github': lambda: GithubFederationProvider(audience=safe_list_get(helper, 1)).get_token(),
'gitlab': lambda: GitlabFederationProvider(token_env_var=safe_list_get(helper, 1)).get_token(),
'spacelift': lambda: SpaceliftFederationProvider().get_token(),
Expand Down
8 changes: 7 additions & 1 deletion src/britive/secrets_manager/policies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Union
from typing import Literal, Union


class PasswordPolicies:
Expand Down Expand Up @@ -201,6 +201,7 @@ def build( # noqa: PLR0913
access_validity_time: int = 120,
approver_users: list = None,
approver_tags: list = None,
manager_condition: Literal['All', 'Any', 'Manager'] = '',
access_type: str = 'Allow',
identifier_type: str = 'name',
condition_as_dict: bool = False,
Expand Down Expand Up @@ -249,6 +250,10 @@ def build( # noqa: PLR0913
If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required.
:param approver_tags: Optional list of tag names who are considered approvers.
If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required.
:param manager_condition: Optional condition to enable requiring user's manager approval. Valid values are
`Any` or `All` or `Manager`. `Any` corresponds to manager approval required, `All` corresponds to
manager and approver_users/approver_tags approval required, and `Manager` corresponds to just the manager's
approval required
:param access_type: The type of access this policy provides. Valid values are `Allow` and `Deny`. Defaults
to `Allow`.
:param identifier_type: Valid values are `id` or `name`. Defaults to `name`. Represents which type of
Expand Down Expand Up @@ -279,6 +284,7 @@ def build( # noqa: PLR0913
access_validity_time=access_validity_time,
approver_users=approver_users,
approver_tags=approver_tags,
manager_condition=manager_condition,
access_type=access_type,
identifier_type=identifier_type,
condition_as_dict=condition_as_dict,
Expand Down
15 changes: 10 additions & 5 deletions src/britive/system/policies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Union
from typing import Literal, Union


class SystemPolicies:
Expand Down Expand Up @@ -168,6 +168,7 @@ def build( # noqa: PLR0913
access_validity_time: int = 120,
approver_users: list = None,
approver_tags: list = None,
manager_condition: Literal['All', 'Any', 'Manager'] = '',
access_type: str = 'Allow',
identifier_type: str = 'name',
condition_as_dict: bool = False,
Expand Down Expand Up @@ -221,6 +222,10 @@ def build( # noqa: PLR0913
If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required.
:param approver_tags: Optional list of tag names who are considered approvers.
If `approval_notification_medium` is set then either `approver_users` or `approver_tags` is required.
:param manager_condition: Optional condition to enable requiring user's manager approval. Valid values are
`Any` or `All` or `Manager`. `Any` corresponds to manager approval required, `All` corresponds to
manager and approver_users/approver_tags approval required, and `Manager` corresponds to just the manager's
approval required
:param access_type: The type of access this policy provides. Valid values are `Allow` and `Deny`. Defaults
to `Allow`.
:param identifier_type: Valid values are `id` or `name`. Defaults to `name`. Represents which type of
Expand All @@ -247,10 +252,8 @@ def build( # noqa: PLR0913

# handle approval logic
if approval_notification_medium:
if not approver_users and not approver_tags:
raise ValueError(
'when approval is required either approver_tags or approver_users or both must be provided'
)
if not approver_users and not approver_tags and manager_condition.capitalize() != 'Manager':
raise ValueError('when approval is required either approver_tags or approver_users must be provided')
approval_condition = {
'notificationMedium': approval_notification_medium,
'timeToApprove': time_to_approve,
Expand All @@ -263,6 +266,8 @@ def build( # noqa: PLR0913
approval_condition['approvers'].pop('userIds')
if not approver_tags:
approval_condition['approvers'].pop('tags')
if manager_condition:
approval_condition['managerApproval'] = {'required': True, 'condition': manager_condition.capitalize()}

condition['approval'] = approval_condition

Expand Down
4 changes: 2 additions & 2 deletions tests/000-global_settings-02-notification_mediums.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

def test_create(cached_notification_medium):
assert isinstance(cached_notification_medium, dict)
assert 'pytest-nm-' in cached_notification_medium['name']
assert 'pysdktest-nm-' in cached_notification_medium['name']


def test_list():
Expand All @@ -20,7 +20,7 @@ def test_get(cached_notification_medium):

def test_update(cached_notification_medium):
r = str(random.randint(0, 999))
new_name = f'{cached_notification_medium["name"]}-{r}'
new_name = f'{r}-{cached_notification_medium["name"]}'[:30]
britive.global_settings.notification_mediums.update(cached_notification_medium['id'], parameters={'name': new_name})
response = britive.global_settings.notification_mediums.get(cached_notification_medium['id'])
assert response['name'] == new_name
2 changes: 1 addition & 1 deletion tests/150-secrets_manager-01-secrets_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_password_policies_list():

def test_password_policies_update(cached_password_policies):
r = str(random.randint(0, 999))
new_name = f'{cached_password_policies["name"]}-{r}'
new_name = f'{r}-{cached_password_policies["name"]}'[:30]
britive.secrets_manager.password_policies.update(cached_password_policies['id'], name=new_name)
assert britive.secrets_manager.password_policies.get(cached_password_policies['id'])['name'] == new_name

Expand Down