cartography 0.110.0rc1__py3-none-any.whl → 0.111.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cartography might be problematic. Click here for more details.

Files changed (87) hide show
  1. cartography/_version.py +16 -3
  2. cartography/cli.py +46 -8
  3. cartography/config.py +16 -9
  4. cartography/data/indexes.cypher +0 -2
  5. cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -2
  6. cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
  7. cartography/graph/querybuilder.py +70 -0
  8. cartography/intel/aws/apigateway.py +113 -4
  9. cartography/intel/aws/cognito.py +201 -0
  10. cartography/intel/aws/ec2/vpc.py +140 -124
  11. cartography/intel/aws/ecs.py +7 -1
  12. cartography/intel/aws/eventbridge.py +73 -0
  13. cartography/intel/aws/glue.py +64 -0
  14. cartography/intel/aws/kms.py +13 -1
  15. cartography/intel/aws/rds.py +105 -0
  16. cartography/intel/aws/resources.py +2 -0
  17. cartography/intel/aws/route53.py +3 -1
  18. cartography/intel/aws/s3.py +104 -0
  19. cartography/intel/entra/__init__.py +41 -43
  20. cartography/intel/entra/applications.py +2 -1
  21. cartography/intel/entra/ou.py +1 -1
  22. cartography/intel/github/__init__.py +21 -25
  23. cartography/intel/github/repos.py +32 -48
  24. cartography/intel/github/util.py +12 -0
  25. cartography/intel/keycloak/__init__.py +153 -0
  26. cartography/intel/keycloak/authenticationexecutions.py +322 -0
  27. cartography/intel/keycloak/authenticationflows.py +77 -0
  28. cartography/intel/keycloak/clients.py +187 -0
  29. cartography/intel/keycloak/groups.py +126 -0
  30. cartography/intel/keycloak/identityproviders.py +94 -0
  31. cartography/intel/keycloak/organizations.py +163 -0
  32. cartography/intel/keycloak/realms.py +61 -0
  33. cartography/intel/keycloak/roles.py +202 -0
  34. cartography/intel/keycloak/scopes.py +73 -0
  35. cartography/intel/keycloak/users.py +70 -0
  36. cartography/intel/keycloak/util.py +47 -0
  37. cartography/intel/kubernetes/__init__.py +4 -0
  38. cartography/intel/kubernetes/rbac.py +464 -0
  39. cartography/intel/kubernetes/util.py +17 -0
  40. cartography/models/aws/apigateway/apigatewaydeployment.py +74 -0
  41. cartography/models/aws/cognito/__init__.py +0 -0
  42. cartography/models/aws/cognito/identity_pool.py +70 -0
  43. cartography/models/aws/cognito/user_pool.py +47 -0
  44. cartography/models/aws/ec2/security_groups.py +1 -1
  45. cartography/models/aws/ec2/vpc.py +46 -0
  46. cartography/models/aws/ec2/vpc_cidr.py +102 -0
  47. cartography/models/aws/ecs/services.py +17 -0
  48. cartography/models/aws/ecs/tasks.py +1 -0
  49. cartography/models/aws/eventbridge/target.py +71 -0
  50. cartography/models/aws/glue/job.py +69 -0
  51. cartography/models/aws/rds/event_subscription.py +146 -0
  52. cartography/models/aws/route53/dnsrecord.py +21 -0
  53. cartography/models/github/dependencies.py +1 -2
  54. cartography/models/keycloak/__init__.py +0 -0
  55. cartography/models/keycloak/authenticationexecution.py +160 -0
  56. cartography/models/keycloak/authenticationflow.py +54 -0
  57. cartography/models/keycloak/client.py +177 -0
  58. cartography/models/keycloak/group.py +101 -0
  59. cartography/models/keycloak/identityprovider.py +89 -0
  60. cartography/models/keycloak/organization.py +116 -0
  61. cartography/models/keycloak/organizationdomain.py +73 -0
  62. cartography/models/keycloak/realm.py +173 -0
  63. cartography/models/keycloak/role.py +126 -0
  64. cartography/models/keycloak/scope.py +73 -0
  65. cartography/models/keycloak/user.py +51 -0
  66. cartography/models/kubernetes/clusterrolebindings.py +98 -0
  67. cartography/models/kubernetes/clusterroles.py +52 -0
  68. cartography/models/kubernetes/rolebindings.py +119 -0
  69. cartography/models/kubernetes/roles.py +76 -0
  70. cartography/models/kubernetes/serviceaccounts.py +77 -0
  71. cartography/models/tailscale/device.py +1 -0
  72. cartography/sync.py +2 -0
  73. cartography/util.py +8 -0
  74. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/METADATA +4 -3
  75. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/RECORD +85 -46
  76. cartography/data/jobs/cleanup/aws_import_vpc_cleanup.json +0 -23
  77. cartography/intel/entra/resources.py +0 -20
  78. /cartography/data/jobs/{analysis → scoped_analysis}/aws_s3acl_analysis.json +0 -0
  79. /cartography/models/aws/{__init__.py → apigateway/__init__.py} +0 -0
  80. /cartography/models/aws/{apigateway.py → apigateway/apigateway.py} +0 -0
  81. /cartography/models/aws/{apigatewaycertificate.py → apigateway/apigatewaycertificate.py} +0 -0
  82. /cartography/models/aws/{apigatewayresource.py → apigateway/apigatewayresource.py} +0 -0
  83. /cartography/models/aws/{apigatewaystage.py → apigateway/apigatewaystage.py} +0 -0
  84. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/WHEEL +0 -0
  85. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/entry_points.txt +0 -0
  86. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/licenses/LICENSE +0 -0
  87. {cartography-0.110.0rc1.dist-info → cartography-0.111.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,77 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.intel.keycloak.util import get_paginated
10
+ from cartography.models.keycloak.authenticationflow import (
11
+ KeycloakAuthenticationFlowSchema,
12
+ )
13
+ from cartography.util import timeit
14
+
15
+ logger = logging.getLogger(__name__)
16
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
17
+ _TIMEOUT = (60, 60)
18
+
19
+
20
+ @timeit
21
+ def sync(
22
+ neo4j_session: neo4j.Session,
23
+ api_session: requests.Session,
24
+ base_url: str,
25
+ common_job_parameters: dict[str, Any],
26
+ ) -> list[dict[str, Any]]:
27
+ authenticationflows = get(
28
+ api_session,
29
+ base_url,
30
+ common_job_parameters["REALM"],
31
+ )
32
+ load_authenticationflows(
33
+ neo4j_session,
34
+ authenticationflows,
35
+ common_job_parameters["REALM"],
36
+ common_job_parameters["UPDATE_TAG"],
37
+ )
38
+ cleanup(neo4j_session, common_job_parameters)
39
+ return authenticationflows
40
+
41
+
42
+ @timeit
43
+ def get(
44
+ api_session: requests.Session,
45
+ base_url: str,
46
+ realm: str,
47
+ ) -> list[dict[str, Any]]:
48
+ url = f"{base_url}/admin/realms/{realm}/authentication/flows"
49
+ return list(get_paginated(api_session, url, params={"briefRepresentation": False}))
50
+
51
+
52
+ @timeit
53
+ def load_authenticationflows(
54
+ neo4j_session: neo4j.Session,
55
+ data: list[dict[str, Any]],
56
+ realm: str,
57
+ update_tag: int,
58
+ ) -> None:
59
+ logger.info(
60
+ "Loading %d Keycloak AuthenticationFlows (%s) into Neo4j.", len(data), realm
61
+ )
62
+ load(
63
+ neo4j_session,
64
+ KeycloakAuthenticationFlowSchema(),
65
+ data,
66
+ LASTUPDATED=update_tag,
67
+ REALM=realm,
68
+ )
69
+
70
+
71
+ @timeit
72
+ def cleanup(
73
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
74
+ ) -> None:
75
+ GraphJob.from_node_schema(
76
+ KeycloakAuthenticationFlowSchema(), common_job_parameters
77
+ ).run(neo4j_session)
@@ -0,0 +1,187 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.client.core.tx import load_matchlinks
9
+ from cartography.graph.job import GraphJob
10
+ from cartography.intel.keycloak.util import get_paginated
11
+ from cartography.models.keycloak.client import KeycloakClientSchema
12
+ from cartography.models.keycloak.client import KeycloakClientToFlowMatchLink
13
+ from cartography.models.keycloak.user import KeycloakUserSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
18
+ _TIMEOUT = (60, 60)
19
+
20
+
21
+ @timeit
22
+ def sync(
23
+ neo4j_session: neo4j.Session,
24
+ api_session: requests.Session,
25
+ base_url: str,
26
+ common_job_parameters: dict[str, Any],
27
+ realm_default_flows: dict[str, Any],
28
+ ) -> list[dict]:
29
+ clients = get(
30
+ api_session,
31
+ base_url,
32
+ common_job_parameters["REALM"],
33
+ )
34
+ transformed_clients, service_accounts, flows_binding = transform(
35
+ clients, realm_default_flows
36
+ )
37
+ load_service_accounts(
38
+ neo4j_session,
39
+ service_accounts,
40
+ common_job_parameters["REALM"],
41
+ common_job_parameters["UPDATE_TAG"],
42
+ )
43
+ load_clients(
44
+ neo4j_session,
45
+ transformed_clients,
46
+ common_job_parameters["REALM"],
47
+ common_job_parameters["UPDATE_TAG"],
48
+ )
49
+ load_flow_bindings(
50
+ neo4j_session,
51
+ flows_binding,
52
+ common_job_parameters["REALM_ID"],
53
+ common_job_parameters["UPDATE_TAG"],
54
+ )
55
+ cleanup(neo4j_session, common_job_parameters)
56
+ return clients
57
+
58
+
59
+ @timeit
60
+ def get(
61
+ api_session: requests.Session,
62
+ base_url: str,
63
+ realm: str,
64
+ ) -> list[dict[str, Any]]:
65
+ result: list[dict[str, Any]] = []
66
+ url = f"{base_url}/admin/realms/{realm}/clients"
67
+ for client in get_paginated(
68
+ api_session, url, params={"briefRepresentation": False}
69
+ ):
70
+ # Check if the client has a service account user
71
+ if "service_account" in client.get("defaultClientScopes", []):
72
+ # Get service account user for each client
73
+ service_account_url = f"{base_url}/admin/realms/{realm}/clients/{client['id']}/service-account-user"
74
+ sa_req = api_session.get(
75
+ service_account_url,
76
+ timeout=_TIMEOUT,
77
+ params={"briefRepresentation": False},
78
+ )
79
+ sa_req.raise_for_status()
80
+ client["service_account_user"] = sa_req.json()
81
+ result.append(client)
82
+ return result
83
+
84
+
85
+ def transform(
86
+ clients: list[dict[str, Any]], default_flows: dict[str, Any]
87
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
88
+ transformed_clients = []
89
+ service_accounts = []
90
+ flow_bindings = []
91
+ for client in clients:
92
+ sa = client.get("service_account_user")
93
+ if sa:
94
+ service_accounts.append(sa)
95
+ client["_service_account_user_id"] = sa["id"]
96
+ client.pop("service_account_user", None)
97
+
98
+ for flow_name, default_flow_id in default_flows.items():
99
+ flow_binding = {
100
+ "client_id": client["id"],
101
+ "flow_name": flow_name,
102
+ "flow_id": default_flow_id,
103
+ "default_flow": True,
104
+ }
105
+ if client.get("authenticationFlowBindingOverrides", {}).get(flow_name):
106
+ flow_binding["flow_id"] = client["authenticationFlowBindingOverrides"][
107
+ flow_name
108
+ ]
109
+ flow_binding["default_flow"] = False
110
+ flow_bindings.append(flow_binding)
111
+
112
+ transformed_clients.append(client)
113
+ return transformed_clients, service_accounts, flow_bindings
114
+
115
+
116
+ @timeit
117
+ def load_clients(
118
+ neo4j_session: neo4j.Session,
119
+ data: list[dict[str, Any]],
120
+ realm: str,
121
+ update_tag: int,
122
+ ) -> None:
123
+ logger.info("Loading %d Keycloak Clients (%s) into Neo4j.", len(data), realm)
124
+ load(
125
+ neo4j_session,
126
+ KeycloakClientSchema(),
127
+ data,
128
+ LASTUPDATED=update_tag,
129
+ REALM=realm,
130
+ )
131
+
132
+
133
+ @timeit
134
+ def load_service_accounts(
135
+ neo4j_session: neo4j.Session,
136
+ data: list[dict[str, Any]],
137
+ realm: str,
138
+ update_tag: int,
139
+ ) -> None:
140
+ logger.info(
141
+ "Loading %d Keycloak Service Accounts (%s) into Neo4j.", len(data), realm
142
+ )
143
+ load(
144
+ neo4j_session,
145
+ KeycloakUserSchema(),
146
+ data,
147
+ LASTUPDATED=update_tag,
148
+ REALM=realm,
149
+ )
150
+
151
+
152
+ @timeit
153
+ def load_flow_bindings(
154
+ neo4j_session: neo4j.Session,
155
+ biddings: list[dict[str, Any]],
156
+ realm_id: str,
157
+ update_tag: int,
158
+ ) -> None:
159
+ load_matchlinks(
160
+ neo4j_session,
161
+ KeycloakClientToFlowMatchLink(),
162
+ biddings,
163
+ LASTUPDATED=update_tag,
164
+ _sub_resource_label="KeycloakRealm",
165
+ _sub_resource_id=realm_id,
166
+ )
167
+
168
+
169
+ @timeit
170
+ def cleanup(
171
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
172
+ ) -> None:
173
+ GraphJob.from_node_schema(KeycloakClientSchema(), common_job_parameters).run(
174
+ neo4j_session
175
+ )
176
+ # It's OK to cleanup users here as it will not clean the regular users (which have the same UPDATE_TAG)
177
+ GraphJob.from_node_schema(KeycloakUserSchema(), common_job_parameters).run(
178
+ neo4j_session
179
+ )
180
+ GraphJob.from_matchlink(
181
+ KeycloakClientToFlowMatchLink(),
182
+ sub_resource_label="KeycloakRealm",
183
+ sub_resource_id=common_job_parameters["REALM_ID"],
184
+ update_tag=common_job_parameters["UPDATE_TAG"],
185
+ ).run(
186
+ neo4j_session,
187
+ )
@@ -0,0 +1,126 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.intel.keycloak.util import get_paginated
10
+ from cartography.models.keycloak.group import KeycloakGroupSchema
11
+ from cartography.util import timeit
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @timeit
17
+ def sync(
18
+ neo4j_session: neo4j.Session,
19
+ api_session: requests.Session,
20
+ base_url: str,
21
+ common_job_parameters: dict[str, Any],
22
+ ) -> None:
23
+ groups = get(
24
+ api_session,
25
+ base_url,
26
+ common_job_parameters["REALM"],
27
+ )
28
+ transformed_groups = transform(groups)
29
+ load_groups(
30
+ neo4j_session,
31
+ transformed_groups,
32
+ common_job_parameters["REALM"],
33
+ common_job_parameters["UPDATE_TAG"],
34
+ )
35
+ cleanup(neo4j_session, common_job_parameters)
36
+
37
+
38
+ @timeit
39
+ def _get_subgroups(
40
+ api_session: requests.Session,
41
+ base_url: str,
42
+ realm: str,
43
+ group_id: str,
44
+ ) -> list[dict[str, Any]]:
45
+ result: list[dict[str, Any]] = []
46
+ url = f"{base_url}/admin/realms/{realm}/groups/{group_id}/children"
47
+ for group in get_paginated(
48
+ api_session,
49
+ url,
50
+ ):
51
+ group["_members"] = _get_members(api_session, base_url, realm, group["id"])
52
+ result.append(group)
53
+ if group.get("subGroupCount", 0) > 0:
54
+ result.extend(_get_subgroups(api_session, base_url, realm, group["id"]))
55
+ return result
56
+
57
+
58
+ @timeit
59
+ def _get_members(
60
+ api_session: requests.Session,
61
+ base_url: str,
62
+ realm: str,
63
+ group_id: str,
64
+ ) -> list[dict[str, Any]]:
65
+ url = f"{base_url}/admin/realms/{realm}/groups/{group_id}/members"
66
+ return list(get_paginated(api_session, url))
67
+
68
+
69
+ @timeit
70
+ def get(
71
+ api_session: requests.Session,
72
+ base_url: str,
73
+ realm: str,
74
+ ) -> list[dict[str, Any]]:
75
+ result: list[dict[str, Any]] = []
76
+
77
+ url = f"{base_url}/admin/realms/{realm}/groups"
78
+ for group in get_paginated(api_session, url, params={"briefRepresentation": False}):
79
+ group["_members"] = _get_members(api_session, base_url, realm, group["id"])
80
+ result.append(group)
81
+ if group.get("subGroupCount", 0) > 0:
82
+ result.extend(_get_subgroups(api_session, base_url, realm, group["id"]))
83
+ return result
84
+
85
+
86
+ def transform(groups: list[dict[str, Any]]) -> list[dict[str, Any]]:
87
+ for group in groups:
88
+ # Transform members to a list of IDs for easier relationship handling
89
+ group["_member_ids"] = [m["id"] for m in group["_members"]]
90
+ group.pop("_members")
91
+ # Transform roles to a list of role names for easier relationship handling
92
+ group["_roles"] = []
93
+ for role_name in group.get("realmRoles", []):
94
+ group["_roles"].append(role_name)
95
+ group.pop("realmRoles", None)
96
+ for roles in group.get("clientRoles", {}).values():
97
+ for role_name in roles:
98
+ group["_roles"].append(role_name)
99
+ group.pop("clientRoles", None)
100
+ return groups
101
+
102
+
103
+ @timeit
104
+ def load_groups(
105
+ neo4j_session: neo4j.Session,
106
+ data: list[dict[str, Any]],
107
+ realm: str,
108
+ update_tag: int,
109
+ ) -> None:
110
+ logger.info("Loading %d Keycloak Groups (%s) into Neo4j.", len(data), realm)
111
+ load(
112
+ neo4j_session,
113
+ KeycloakGroupSchema(),
114
+ data,
115
+ LASTUPDATED=update_tag,
116
+ REALM=realm,
117
+ )
118
+
119
+
120
+ @timeit
121
+ def cleanup(
122
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
123
+ ) -> None:
124
+ GraphJob.from_node_schema(KeycloakGroupSchema(), common_job_parameters).run(
125
+ neo4j_session
126
+ )
@@ -0,0 +1,94 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.intel.keycloak.util import get_paginated
10
+ from cartography.models.keycloak.identityprovider import KeycloakIdentityProviderSchema
11
+ from cartography.util import timeit
12
+
13
+ logger = logging.getLogger(__name__)
14
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
15
+ _TIMEOUT = (60, 60)
16
+
17
+
18
+ @timeit
19
+ def sync(
20
+ neo4j_session: neo4j.Session,
21
+ api_session: requests.Session,
22
+ base_url: str,
23
+ common_job_parameters: dict[str, Any],
24
+ ) -> None:
25
+ identityproviders = get(
26
+ api_session,
27
+ base_url,
28
+ common_job_parameters["REALM"],
29
+ )
30
+ idps_transformed = transform(identityproviders)
31
+ load_identityproviders(
32
+ neo4j_session,
33
+ idps_transformed,
34
+ common_job_parameters["REALM"],
35
+ common_job_parameters["UPDATE_TAG"],
36
+ )
37
+ cleanup(neo4j_session, common_job_parameters)
38
+
39
+
40
+ @timeit
41
+ def get(
42
+ api_session: requests.Session,
43
+ base_url: str,
44
+ realm: str,
45
+ ) -> list[dict[str, Any]]:
46
+ result: list[dict[str, Any]] = []
47
+ url = f"{base_url}/admin/realms/{realm}/identity-provider/instances"
48
+ for idp in get_paginated(api_session, url, params={"briefRepresentation": False}):
49
+ # Get members
50
+ members_url = f"{base_url}/admin/realms/{realm}/users"
51
+ idp["_members"] = list(
52
+ get_paginated(
53
+ api_session,
54
+ members_url,
55
+ params={"idpAlias": idp["alias"], "briefRepresentation": True},
56
+ )
57
+ )
58
+ result.append(idp)
59
+ return result
60
+
61
+
62
+ def transform(idps: list[dict[str, Any]]) -> list[dict[str, Any]]:
63
+ for idp in idps:
64
+ idp["_member_ids"] = [member["id"] for member in idp["_members"]]
65
+ idp.pop("_members", None)
66
+ return idps
67
+
68
+
69
+ @timeit
70
+ def load_identityproviders(
71
+ neo4j_session: neo4j.Session,
72
+ data: list[dict[str, Any]],
73
+ realm: str,
74
+ update_tag: int,
75
+ ) -> None:
76
+ logger.info(
77
+ "Loading %d Keycloak IdentityProviders (%s) into Neo4j.", len(data), realm
78
+ )
79
+ load(
80
+ neo4j_session,
81
+ KeycloakIdentityProviderSchema(),
82
+ data,
83
+ LASTUPDATED=update_tag,
84
+ REALM=realm,
85
+ )
86
+
87
+
88
+ @timeit
89
+ def cleanup(
90
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
91
+ ) -> None:
92
+ GraphJob.from_node_schema(
93
+ KeycloakIdentityProviderSchema(), common_job_parameters
94
+ ).run(neo4j_session)
@@ -0,0 +1,163 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Tuple
4
+
5
+ import neo4j
6
+ import requests
7
+
8
+ from cartography.client.core.tx import load
9
+ from cartography.graph.job import GraphJob
10
+ from cartography.intel.keycloak.util import get_paginated
11
+ from cartography.models.keycloak.organization import KeycloakOrganizationSchema
12
+ from cartography.models.keycloak.organizationdomain import (
13
+ KeycloakOrganizationDomainSchema,
14
+ )
15
+ from cartography.util import timeit
16
+
17
+ logger = logging.getLogger(__name__)
18
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
19
+ _TIMEOUT = (60, 60)
20
+
21
+
22
+ @timeit
23
+ def sync(
24
+ neo4j_session: neo4j.Session,
25
+ api_session: requests.Session,
26
+ base_url: str,
27
+ common_job_parameters: dict[str, Any],
28
+ ) -> None:
29
+ organizations = get(
30
+ api_session,
31
+ base_url,
32
+ common_job_parameters["REALM"],
33
+ )
34
+ transformed_orgs, transformed_domains = transform(organizations)
35
+ load_organizations(
36
+ neo4j_session,
37
+ transformed_orgs,
38
+ common_job_parameters["REALM"],
39
+ common_job_parameters["UPDATE_TAG"],
40
+ )
41
+ load_org_domains(
42
+ neo4j_session,
43
+ transformed_domains,
44
+ common_job_parameters["REALM"],
45
+ common_job_parameters["UPDATE_TAG"],
46
+ )
47
+ cleanup(neo4j_session, common_job_parameters)
48
+
49
+
50
+ def transform(
51
+ organizations: list[dict[str, Any]],
52
+ ) -> Tuple[list[dict[str, Any]], list[dict[str, Any]]]:
53
+ transformed_orgs = []
54
+ transformed_domains = {}
55
+ for org in organizations:
56
+ # Transform members to a list of IDs
57
+ org["_managed_members"] = []
58
+ org["_unmanaged_members"] = []
59
+ for member in org.get("_members", []):
60
+ if member.get("membershipType") == "UNMANAGED":
61
+ org["_unmanaged_members"].append(member["id"])
62
+ else:
63
+ org["_managed_members"].append(member["id"])
64
+ org.pop("_members", None)
65
+ # Transform identity providers to a list of IDs
66
+ org["_idp_ids"] = [
67
+ idp["internalId"] for idp in org.get("_identity_providers", [])
68
+ ]
69
+ org.pop("_identity_providers", None)
70
+ # Extract domains
71
+ domains = org.get("domains", [])
72
+ for domain in domains:
73
+ domain_id = f"{org['id']}-{domain['name']}"
74
+ transformed_domains[domain_id] = {
75
+ "id": domain_id,
76
+ "verified": domain.get("verified", False),
77
+ "name": domain["name"],
78
+ "organization_id": org["id"],
79
+ }
80
+ org.pop("domains", None)
81
+ transformed_orgs.append(org)
82
+ return transformed_orgs, list(transformed_domains.values())
83
+
84
+
85
+ @timeit
86
+ def get(
87
+ api_session: requests.Session,
88
+ base_url: str,
89
+ realm: str,
90
+ ) -> list[dict[str, Any]]:
91
+ result: list[dict[str, Any]] = []
92
+ url = f"{base_url}/admin/realms/{realm}/organizations"
93
+ for org in get_paginated(api_session, url):
94
+ # Get members
95
+ members_url = (
96
+ f"{base_url}/admin/realms/{realm}/organizations/{org['id']}/members"
97
+ )
98
+ org["_members"] = list(
99
+ get_paginated(
100
+ api_session,
101
+ members_url,
102
+ params={"briefRepresentation": True},
103
+ )
104
+ )
105
+ # Get Identity Providers
106
+ idp_url = f"{base_url}/admin/realms/{realm}/organizations/{org['id']}/identity-providers"
107
+ org["_identity_providers"] = list(
108
+ get_paginated(
109
+ api_session,
110
+ idp_url,
111
+ params={"briefRepresentation": True},
112
+ )
113
+ )
114
+ result.append(org)
115
+ return result
116
+
117
+
118
+ @timeit
119
+ def load_organizations(
120
+ neo4j_session: neo4j.Session,
121
+ data: list[dict[str, Any]],
122
+ realm: str,
123
+ update_tag: int,
124
+ ) -> None:
125
+ logger.info("Loading %d Keycloak Organizations (%s) into Neo4j.", len(data), realm)
126
+ load(
127
+ neo4j_session,
128
+ KeycloakOrganizationSchema(),
129
+ data,
130
+ LASTUPDATED=update_tag,
131
+ REALM=realm,
132
+ )
133
+
134
+
135
+ @timeit
136
+ def load_org_domains(
137
+ neo4j_session: neo4j.Session,
138
+ data: list[dict[str, Any]],
139
+ realm: str,
140
+ update_tag: int,
141
+ ) -> None:
142
+ logger.info(
143
+ "Loading %d Keycloak Organization Domains (%s) into Neo4j.", len(data), realm
144
+ )
145
+ load(
146
+ neo4j_session,
147
+ KeycloakOrganizationDomainSchema(),
148
+ data,
149
+ LASTUPDATED=update_tag,
150
+ REALM=realm,
151
+ )
152
+
153
+
154
+ @timeit
155
+ def cleanup(
156
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
157
+ ) -> None:
158
+ GraphJob.from_node_schema(
159
+ KeycloakOrganizationDomainSchema(), common_job_parameters
160
+ ).run(neo4j_session)
161
+ GraphJob.from_node_schema(KeycloakOrganizationSchema(), common_job_parameters).run(
162
+ neo4j_session
163
+ )
@@ -0,0 +1,61 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import requests
6
+
7
+ from cartography.client.core.tx import load
8
+ from cartography.graph.job import GraphJob
9
+ from cartography.models.keycloak.realm import KeycloakRealmSchema
10
+ from cartography.util import timeit
11
+
12
+ logger = logging.getLogger(__name__)
13
+ # Connect and read timeouts of 60 seconds each; see https://requests.readthedocs.io/en/master/user/advanced/#timeouts
14
+ _TIMEOUT = (60, 60)
15
+
16
+
17
+ @timeit
18
+ def sync(
19
+ neo4j_session: neo4j.Session,
20
+ api_session: requests.Session,
21
+ base_url: str,
22
+ common_job_parameters: dict[str, Any],
23
+ ) -> list[dict]:
24
+ realms = get(api_session, base_url)
25
+ load_realms(neo4j_session, realms, common_job_parameters["UPDATE_TAG"])
26
+ cleanup(neo4j_session, common_job_parameters)
27
+ return realms
28
+
29
+
30
+ @timeit
31
+ def get(
32
+ api_session: requests.Session,
33
+ base_url: str,
34
+ ) -> list[dict[str, Any]]:
35
+ req = api_session.get(f"{base_url}/admin/realms", timeout=_TIMEOUT)
36
+ req.raise_for_status()
37
+ return req.json()
38
+
39
+
40
+ @timeit
41
+ def load_realms(
42
+ neo4j_session: neo4j.Session,
43
+ data: list[dict[str, Any]],
44
+ update_tag: int,
45
+ ) -> None:
46
+ logger.info("Loading %d Keycloak Realms into Neo4j.", len(data))
47
+ load(
48
+ neo4j_session,
49
+ KeycloakRealmSchema(),
50
+ data,
51
+ LASTUPDATED=update_tag,
52
+ )
53
+
54
+
55
+ @timeit
56
+ def cleanup(
57
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
58
+ ) -> None:
59
+ GraphJob.from_node_schema(KeycloakRealmSchema(), common_job_parameters).run(
60
+ neo4j_session
61
+ )