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
406 changes: 406 additions & 0 deletions docs/SUPPORT_GUIDE_SSO_ORPHANED_MAPPINGS.md

Large diffs are not rendered by default.

400 changes: 400 additions & 0 deletions docs/sso_troubleshooting.md

Large diffs are not rendered by default.

41 changes: 40 additions & 1 deletion pyatlan/client/aio/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, List

from pydantic.v1 import validate_arguments
Expand All @@ -23,6 +24,8 @@
if TYPE_CHECKING:
pass

logger = logging.getLogger(__name__)


class AsyncSSOClient:
"""
Expand Down Expand Up @@ -63,14 +66,35 @@ async def create_group_mapping(
:param atlan_group: existing Atlan group.
:param sso_group_name: name of the SSO group.
:raises AtlanError: on any error during API invocation.
:raises InvalidRequestError: if the group doesn't have required fields or mapping already exists.
:returns: created SSO group mapping instance.
"""
# Validate the group has required fields
if not atlan_group.id:
raise ErrorCode.SSO_MAPPING_VALIDATION_ERROR.exception_with_parameters(
"Atlan group must have an 'id' field populated",
sso_alias,
)
if not atlan_group.name:
raise ErrorCode.SSO_MAPPING_VALIDATION_ERROR.exception_with_parameters(
"Atlan group must have a 'name' field populated",
sso_alias,
)

logger.info(
f"Creating SSO group mapping: {atlan_group.alias} (ID: {atlan_group.id}) "
f"<-> {sso_group_name} (SSO: {sso_alias})"
)

await self._check_existing_group_mappings(sso_alias, atlan_group)
endpoint, request_obj = SSOCreateGroupMapping.prepare_request(
sso_alias, atlan_group, sso_group_name
)
raw_json = await self._client._call_api(endpoint, request_obj=request_obj)
return SSOCreateGroupMapping.process_response(raw_json)
result = SSOCreateGroupMapping.process_response(raw_json)

logger.info(f"Successfully created SSO group mapping with ID: {result.id}")
return result

@validate_arguments
async def update_group_mapping(
Expand Down Expand Up @@ -130,13 +154,28 @@ async def delete_group_mapping(self, sso_alias: str, group_map_id: str) -> None:
"""
Deletes an existing Atlan SSO group mapping.

Note: This only deletes the SSO mapping (identity provider mapper).
If you're experiencing issues with Okta Push Groups failing with
stale externalId errors, you may need to run the diagnostic script
to identify and clean up orphaned mappings.

:param sso_alias: name of the SSO provider.
:param group_map_id: existing SSO group map identifier.
:raises AtlanError: on any error during API invocation.
:returns: an empty response (`None`).
"""
logger.info(f"Deleting SSO group mapping: {group_map_id} (SSO: {sso_alias})")

endpoint, request_obj = SSODeleteGroupMapping.prepare_request(
sso_alias, group_map_id
)
raw_json = await self._client._call_api(endpoint, request_obj=request_obj)

logger.info(f"Successfully deleted SSO group mapping: {group_map_id}")
logger.debug(
"If you're experiencing Okta Push Groups issues with stale externalId, "
"run: python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings "
f"--mode diagnose --sso-alias {sso_alias}"
)

return raw_json
41 changes: 40 additions & 1 deletion pyatlan/client/sso.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from typing import List

from pydantic.v1 import validate_arguments
Expand All @@ -15,6 +16,8 @@
from pyatlan.model.group import AtlanGroup
from pyatlan.model.sso import SSOMapper

logger = logging.getLogger(__name__)


class SSOClient:
"""
Expand Down Expand Up @@ -55,14 +58,35 @@ def create_group_mapping(
:param atlan_group: existing Atlan group.
:param sso_group_name: name of the SSO group.
:raises AtlanError: on any error during API invocation.
:raises InvalidRequestError: if the group doesn't have required fields or mapping already exists.
:returns: created SSO group mapping instance.
"""
# Validate the group has required fields
if not atlan_group.id:
raise ErrorCode.SSO_MAPPING_VALIDATION_ERROR.exception_with_parameters(
"Atlan group must have an 'id' field populated",
sso_alias,
)
if not atlan_group.name:
raise ErrorCode.SSO_MAPPING_VALIDATION_ERROR.exception_with_parameters(
"Atlan group must have a 'name' field populated",
sso_alias,
)

logger.info(
f"Creating SSO group mapping: {atlan_group.alias} (ID: {atlan_group.id}) "
f"<-> {sso_group_name} (SSO: {sso_alias})"
)

self._check_existing_group_mappings(sso_alias, atlan_group)
endpoint, request_obj = SSOCreateGroupMapping.prepare_request(
sso_alias, atlan_group, sso_group_name
)
raw_json = self._client._call_api(endpoint, request_obj=request_obj)
return SSOCreateGroupMapping.process_response(raw_json)
result = SSOCreateGroupMapping.process_response(raw_json)

logger.info(f"Successfully created SSO group mapping with ID: {result.id}")
return result

@validate_arguments
def update_group_mapping(
Expand Down Expand Up @@ -122,13 +146,28 @@ def delete_group_mapping(self, sso_alias: str, group_map_id: str) -> None:
"""
Deletes an existing Atlan SSO group mapping.

Note: This only deletes the SSO mapping (identity provider mapper).
If you're experiencing issues with Okta Push Groups failing with
stale externalId errors, you may need to run the diagnostic script
to identify and clean up orphaned mappings.

:param sso_alias: name of the SSO provider.
:param group_map_id: existing SSO group map identifier.
:raises AtlanError: on any error during API invocation.
:returns: an empty response (`None`).
"""
logger.info(f"Deleting SSO group mapping: {group_map_id} (SSO: {sso_alias})")

endpoint, request_obj = SSODeleteGroupMapping.prepare_request(
sso_alias, group_map_id
)
raw_json = self._client._call_api(endpoint, request_obj=request_obj)

logger.info(f"Successfully deleted SSO group mapping: {group_map_id}")
logger.debug(
"If you're experiencing Okta Push Groups issues with stale externalId, "
"run: python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings "
f"--mode diagnose --sso-alias {sso_alias}"
)

return raw_json
18 changes: 18 additions & 0 deletions pyatlan/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,24 @@ class ErrorCode(Enum):
"You can use SSOClient.update_group_mapping() to update the existing group mapping.",
InvalidRequestError,
)
SSO_GROUP_NOT_FOUND = (
404,
"ATLAN-PYTHON-404-031",
"Atlan group '{0}' (ID: {1}) not found. This may indicate an orphaned SSO group mapping.",
"The SSO mapping references a group that no longer exists. "
"Run the diagnostic script to identify and clean up orphaned mappings: "
"python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings --mode diagnose --sso-alias {2}",
NotFoundError,
)
SSO_MAPPING_VALIDATION_ERROR = (
400,
"ATLAN-PYTHON-400-059",
"SSO mapping validation failed: {0}",
"Ensure the Atlan group exists before creating an SSO mapping. "
"You may have orphaned mappings that need cleanup. "
"Run: python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings --mode diagnose --sso-alias {1}",
InvalidRequestError,
)
INVALID_UPLOAD_FILE_PATH = (
400,
"ATLAN-PYTHON-400-059",
Expand Down
89 changes: 89 additions & 0 deletions pyatlan/samples/sso/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SSO Group Mapping Utilities

This directory contains utilities for managing SSO (Single Sign-On) group mappings in Atlan.

## Available Tools

### Diagnose Orphaned Group Mappings

**File:** `diagnose_orphaned_group_mappings.py`

A diagnostic and cleanup utility for identifying and resolving orphaned SSO group mappings that can cause Okta Push Groups to fail with stale `externalId` errors.

#### Quick Start

```bash
# Set up environment
export ATLAN_BASE_URL="https://your-tenant.atlan.com"
export ATLAN_API_KEY="your-api-key"

# Diagnose all SSO group mappings
python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings \
--mode diagnose \
--sso-alias okta

# Diagnose a specific group
python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings \
--mode diagnose \
--sso-alias okta \
--group-name grpAtlanProdWorkflowAdmin

# Clean up orphaned mappings (interactive)
python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings \
--mode cleanup \
--sso-alias okta

# List all SSO group mappings
python -m pyatlan.samples.sso.diagnose_orphaned_group_mappings \
--mode list \
--sso-alias okta
```

#### Common Use Cases

1. **Okta Push Groups Failing with Stale externalId**
- Run diagnostic to identify orphaned mappings
- Clean up orphaned mappings
- In Okta, unlink and re-link the group (use "Link Group", not "Create")

2. **Regular Maintenance**
- Periodically run diagnostic to catch orphaned mappings early
- Schedule as part of your regular Atlan maintenance routine

3. **Group Migration or Cleanup**
- Before deleting groups, check for associated SSO mappings
- Clean up mappings before deleting the group to avoid orphans

#### Options

- `--mode`: Operation mode
- `diagnose`: Check for orphaned mappings (recommended first step)
- `cleanup`: Remove orphaned mappings (interactive by default)
- `list`: Display all SSO group mappings

- `--sso-alias`: SSO provider alias (e.g., `okta`, `azure`, `jumpcloud`)

- `--group-name`: (Optional) Check only a specific group

- `--non-interactive`: Run cleanup without prompting (use with caution!)

## Documentation

For detailed information about SSO group mapping troubleshooting, see:
- [SSO Troubleshooting Guide](../../../docs/sso_troubleshooting.md)
- [SSO Client API Documentation](../../../docs/client/sso.rst)

## Support

If you encounter issues or need help:

1. Run the diagnostic script and save the output
2. Check the [SSO Troubleshooting Guide](../../../docs/sso_troubleshooting.md)
3. If the issue persists, contact Atlan Support with:
- Diagnostic output
- Full error messages
- Steps you've already tried

## Related Issues

- **LINTEST-425**: Okta Push Groups stale externalId / orphaned mapping issue
8 changes: 8 additions & 0 deletions pyatlan/samples/sso/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025 Atlan Pte. Ltd.
"""
SSO (Single Sign-On) utilities for Atlan.

This module provides utilities for managing and troubleshooting
SSO group mappings in Atlan.
"""
Loading
Loading