cloudconsolelink 4.1.1__tar.gz → 4.2.0__tar.gz
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.
- {cloudconsolelink-4.1.1/cloudconsolelink.egg-info → cloudconsolelink-4.2.0}/PKG-INFO +26 -2
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/README.md +25 -1
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/aws/__init__.py +85 -8
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/aws/links.py +49 -24
- cloudconsolelink-4.2.0/cloudconsolelink/clouds/azure/__init__.py +160 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/gcp/__init__.py +55 -2
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/gcp/links.py +166 -16
- cloudconsolelink-4.2.0/cloudconsolelink/clouds/git/__init__.py +107 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/oci/links.py +90 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0/cloudconsolelink.egg-info}/PKG-INFO +26 -2
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink.egg-info/SOURCES.txt +2 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/tests/test_aws.py +145 -0
- cloudconsolelink-4.2.0/tests/test_azure.py +184 -0
- cloudconsolelink-4.2.0/tests/test_gcp.py +228 -0
- cloudconsolelink-4.2.0/tests/test_git.py +104 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/tests/test_oci.py +74 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/tests/test_provider_coverage.py +15 -0
- cloudconsolelink-4.1.1/cloudconsolelink/clouds/azure/__init__.py +0 -51
- cloudconsolelink-4.1.1/tests/test_azure.py +0 -37
- cloudconsolelink-4.1.1/tests/test_gcp.py +0 -119
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/LICENSE +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/MANIFEST.in +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/__init__.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/__init__.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/aws/errors.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/oci/__init__.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink/clouds/registry/__init__.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink.egg-info/dependency_links.txt +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink.egg-info/requires.txt +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/cloudconsolelink.egg-info/top_level.txt +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/requirements-dev.txt +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/requirements.txt +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/setup.cfg +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/setup.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/tests/test_packaging.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/tests/test_registry_link.py +0 -0
- {cloudconsolelink-4.1.1 → cloudconsolelink-4.2.0}/tests/test_registry_parse.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudconsolelink
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: Generate console links for cloud resources
|
|
5
5
|
Home-page: https://github.com/Cloudanix/cloud-console-links
|
|
6
6
|
Maintainer: Cloudanix
|
|
@@ -31,7 +31,8 @@ Dynamic: provides-extra
|
|
|
31
31
|
Dynamic: summary
|
|
32
32
|
|
|
33
33
|
# What is cloudconsolelink?
|
|
34
|
-
This project for generate console links for AWS, GCP and
|
|
34
|
+
This project for generate console links for AWS, GCP, Azure and OCI cloud resources, and web links for
|
|
35
|
+
GitHub, GitLab, Bitbucket and Azure DevOps entities
|
|
35
36
|
|
|
36
37
|
# Install
|
|
37
38
|
Run ```pip install cloudconsolelink``` to install our code
|
|
@@ -65,6 +66,12 @@ make check
|
|
|
65
66
|
#### For GCP
|
|
66
67
|
```from cloudconsolelink.clouds.gcp import GCPLinker```
|
|
67
68
|
|
|
69
|
+
#### For OCI
|
|
70
|
+
```from cloudconsolelink.clouds.oci import OCILinker```
|
|
71
|
+
|
|
72
|
+
#### For GitHub, GitLab, Bitbucket, Azure DevOps
|
|
73
|
+
```from cloudconsolelink.clouds.git import GitLinker```
|
|
74
|
+
|
|
68
75
|
#### For container images (ECR, GAR/GCR, ACR, Docker Hub, OCIR)
|
|
69
76
|
```from cloudconsolelink.clouds.registry import image_console_link```
|
|
70
77
|
|
|
@@ -141,6 +148,21 @@ console_link = image_console_link(
|
|
|
141
148
|
)
|
|
142
149
|
```
|
|
143
150
|
|
|
151
|
+
#### Git providers:
|
|
152
|
+
```python
|
|
153
|
+
from cloudconsolelink.clouds.git import GitLinker
|
|
154
|
+
|
|
155
|
+
git = GitLinker()
|
|
156
|
+
|
|
157
|
+
git.get_console_link("github", "branch", owner="acme", repo="api", branch="main")
|
|
158
|
+
# https://github.com/acme/api/tree/main
|
|
159
|
+
|
|
160
|
+
# Self-hosted GitHub Enterprise / GitLab: pass base_url
|
|
161
|
+
git.get_console_link("gitlab", "project", base_url="https://gitlab.acme.com", project_path="platform/api")
|
|
162
|
+
# https://gitlab.acme.com/platform/api
|
|
163
|
+
```
|
|
164
|
+
Resource names per provider are the keys of `LINKS` in `cloudconsolelink/clouds/git/__init__.py`.
|
|
165
|
+
|
|
144
166
|
## get_console_link() parameters description:
|
|
145
167
|
|
|
146
168
|
### AWS:
|
|
@@ -187,6 +209,8 @@ console_link = image_console_link(
|
|
|
187
209
|
31) firestore_collection_name
|
|
188
210
|
32) cloud_run_service_name
|
|
189
211
|
|
|
212
|
+
The full, current list of GCP resource names is the `resources` dict in `cloudconsolelink/clouds/gcp/__init__.py`.
|
|
213
|
+
|
|
190
214
|
|
|
191
215
|
# Who uses cloudconsolelink?
|
|
192
216
|
1. [Cloudanix](https://www.cloudanix.com/)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# What is cloudconsolelink?
|
|
2
|
-
This project for generate console links for AWS, GCP and
|
|
2
|
+
This project for generate console links for AWS, GCP, Azure and OCI cloud resources, and web links for
|
|
3
|
+
GitHub, GitLab, Bitbucket and Azure DevOps entities
|
|
3
4
|
|
|
4
5
|
# Install
|
|
5
6
|
Run ```pip install cloudconsolelink``` to install our code
|
|
@@ -33,6 +34,12 @@ make check
|
|
|
33
34
|
#### For GCP
|
|
34
35
|
```from cloudconsolelink.clouds.gcp import GCPLinker```
|
|
35
36
|
|
|
37
|
+
#### For OCI
|
|
38
|
+
```from cloudconsolelink.clouds.oci import OCILinker```
|
|
39
|
+
|
|
40
|
+
#### For GitHub, GitLab, Bitbucket, Azure DevOps
|
|
41
|
+
```from cloudconsolelink.clouds.git import GitLinker```
|
|
42
|
+
|
|
36
43
|
#### For container images (ECR, GAR/GCR, ACR, Docker Hub, OCIR)
|
|
37
44
|
```from cloudconsolelink.clouds.registry import image_console_link```
|
|
38
45
|
|
|
@@ -109,6 +116,21 @@ console_link = image_console_link(
|
|
|
109
116
|
)
|
|
110
117
|
```
|
|
111
118
|
|
|
119
|
+
#### Git providers:
|
|
120
|
+
```python
|
|
121
|
+
from cloudconsolelink.clouds.git import GitLinker
|
|
122
|
+
|
|
123
|
+
git = GitLinker()
|
|
124
|
+
|
|
125
|
+
git.get_console_link("github", "branch", owner="acme", repo="api", branch="main")
|
|
126
|
+
# https://github.com/acme/api/tree/main
|
|
127
|
+
|
|
128
|
+
# Self-hosted GitHub Enterprise / GitLab: pass base_url
|
|
129
|
+
git.get_console_link("gitlab", "project", base_url="https://gitlab.acme.com", project_path="platform/api")
|
|
130
|
+
# https://gitlab.acme.com/platform/api
|
|
131
|
+
```
|
|
132
|
+
Resource names per provider are the keys of `LINKS` in `cloudconsolelink/clouds/git/__init__.py`.
|
|
133
|
+
|
|
112
134
|
## get_console_link() parameters description:
|
|
113
135
|
|
|
114
136
|
### AWS:
|
|
@@ -155,6 +177,8 @@ console_link = image_console_link(
|
|
|
155
177
|
31) firestore_collection_name
|
|
156
178
|
32) cloud_run_service_name
|
|
157
179
|
|
|
180
|
+
The full, current list of GCP resource names is the `resources` dict in `cloudconsolelink/clouds/gcp/__init__.py`.
|
|
181
|
+
|
|
158
182
|
|
|
159
183
|
# Who uses cloudconsolelink?
|
|
160
184
|
1. [Cloudanix](https://www.cloudanix.com/)
|
|
@@ -2,9 +2,14 @@ import logging
|
|
|
2
2
|
import re
|
|
3
3
|
import urllib.parse
|
|
4
4
|
from typing import Dict
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
|
-
from .errors import ARNTooShortError
|
|
7
|
-
from .
|
|
7
|
+
from .errors import ARNTooShortError
|
|
8
|
+
from .errors import InvalidARNError
|
|
9
|
+
from .errors import InvalidPartitionError
|
|
10
|
+
from .errors import InvalidServiceError
|
|
11
|
+
from .links import get_links
|
|
12
|
+
from .links import HOME_URLS
|
|
8
13
|
|
|
9
14
|
logger = logging.getLogger(__name__)
|
|
10
15
|
|
|
@@ -20,16 +25,19 @@ def _render(template: str, data: Dict, arn: str = "") -> str:
|
|
|
20
25
|
produce spaces inside expressions). Unknown expressions render as empty string.
|
|
21
26
|
"""
|
|
22
27
|
resource = str(data.get("resource", ""))
|
|
28
|
+
region = str(data.get("region", ""))
|
|
23
29
|
ctx: Dict[str, str] = {
|
|
30
|
+
**_resource_part_ctx(resource),
|
|
24
31
|
'data.get("region","")': str(data.get("region", "")),
|
|
25
32
|
'data.get("console","")': str(data.get("console", "")),
|
|
26
33
|
'data.get("resource","")': resource,
|
|
27
34
|
'data.get("account","")': str(data.get("account", "")),
|
|
28
35
|
'data.get("service","")': str(data.get("service", "")),
|
|
29
|
-
|
|
36
|
+
"get_arn_string(data)": get_arn_string(data),
|
|
30
37
|
'get_resource_path(data.get("resource",""))': get_resource_path(resource),
|
|
31
38
|
"arn": arn,
|
|
32
|
-
"
|
|
39
|
+
"partition": str(data.get("partition", "")),
|
|
40
|
+
"region": region,
|
|
33
41
|
"console": str(data.get("console", "")),
|
|
34
42
|
"resource": resource,
|
|
35
43
|
"account": str(data.get("account", "")),
|
|
@@ -37,12 +45,50 @@ def _render(template: str, data: Dict, arn: str = "") -> str:
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
def _sub(m: re.Match) -> str:
|
|
40
|
-
expr = re.sub(
|
|
48
|
+
expr = re.sub(
|
|
49
|
+
r"\s+",
|
|
50
|
+
"",
|
|
51
|
+
m.group(1),
|
|
52
|
+
) # collapse whitespace from line continuations
|
|
41
53
|
if expr not in ctx:
|
|
42
54
|
logger.debug("Unknown template expression %r", expr)
|
|
43
55
|
return ctx.get(expr, "")
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
link = _EXPR_RE.sub(_sub, template).replace(" ", "")
|
|
58
|
+
return link if region else _drop_empty_region(link)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resource_part_ctx(resource: str) -> Dict[str, str]:
|
|
62
|
+
"""Template keys for the pieces of a path-style resource.
|
|
63
|
+
|
|
64
|
+
resource_part(N) is the Nth "/"-separated segment (ecs "service/cluster/svc" -> cluster, svc),
|
|
65
|
+
resource_tail is everything after the first "/" (autoscaling "uuid:autoScalingGroupName/name" -> name),
|
|
66
|
+
log_group_id is the log group name in the double-encoded form the CloudWatch console expects.
|
|
67
|
+
"""
|
|
68
|
+
raw = urllib.parse.unquote(resource)
|
|
69
|
+
parts = raw.split("/")
|
|
70
|
+
ctx = {
|
|
71
|
+
f"resource_part({i})": urllib.parse.quote(part, safe="")
|
|
72
|
+
for i, part in enumerate(parts)
|
|
73
|
+
}
|
|
74
|
+
ctx["resource_tail"] = urllib.parse.quote(raw.split("/", 1)[-1], safe="")
|
|
75
|
+
log_group = raw[: -len(":*")] if raw.endswith(":*") else raw
|
|
76
|
+
ctx["log_group_id"] = urllib.parse.quote(
|
|
77
|
+
urllib.parse.quote(log_group, safe=""),
|
|
78
|
+
safe="",
|
|
79
|
+
).replace("%", "$")
|
|
80
|
+
return ctx
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _drop_empty_region(link: str) -> str:
|
|
84
|
+
"""Remove the empty region a region-less ARN (IAM, Route 53, SSO, ...) leaves in a template.
|
|
85
|
+
|
|
86
|
+
"https://.console..." becomes "https://console..." and a bare "region=" query parameter is dropped.
|
|
87
|
+
"""
|
|
88
|
+
link = link.replace("https://.", "https://", 1)
|
|
89
|
+
link = link.replace("?region=&", "?")
|
|
90
|
+
link = re.sub(r"[?&]region=(?=#|$)", "", link)
|
|
91
|
+
return re.sub(r"®ion=(?=&)", "", link)
|
|
46
92
|
|
|
47
93
|
|
|
48
94
|
def get_console(partition: str) -> str:
|
|
@@ -126,6 +172,10 @@ class AWSLinker:
|
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
resource_tokens = tokens[5:]
|
|
175
|
+
# API Gateway ARNs are paths: arn:aws:apigateway:us-east-1::/restapis/abc123. Drop the
|
|
176
|
+
# leading "/" so the first segment is the resource type, as in every other path-style ARN.
|
|
177
|
+
if resource_tokens and resource_tokens[0].startswith("/"):
|
|
178
|
+
resource_tokens[0] = resource_tokens[0][1:]
|
|
129
179
|
|
|
130
180
|
console = get_console(str(data.get("partition", "")))
|
|
131
181
|
if not console:
|
|
@@ -169,14 +219,41 @@ class AWSLinker:
|
|
|
169
219
|
logger.debug(f"Invalid AWS ARN {arn}")
|
|
170
220
|
raise InvalidARNError(f"Invalid AWS ARN {arn}")
|
|
171
221
|
|
|
172
|
-
|
|
222
|
+
# An SNS subscription ARN is the topic ARN plus a subscription id: sns:...:topic:uuid.
|
|
223
|
+
if data["service"] == "sns" and len(resource_tokens) == 2:
|
|
224
|
+
data["resourceType"] = "subscription"
|
|
225
|
+
|
|
226
|
+
template = _find_template(
|
|
227
|
+
links[data["service"]],
|
|
228
|
+
str(data["resourceType"]),
|
|
229
|
+
str(data["resource"]),
|
|
230
|
+
)
|
|
231
|
+
if not template:
|
|
173
232
|
logger.debug(
|
|
174
233
|
f"AWS service {data['service']} resource type {data['resourceType']} not supported",
|
|
175
234
|
)
|
|
176
235
|
return get_service_home_link(data)
|
|
177
236
|
|
|
178
237
|
data["resource"] = urllib.parse.quote(str(data["resource"]))
|
|
179
|
-
return _render(
|
|
238
|
+
return _render(template, data, arn)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _find_template(
|
|
242
|
+
service_links: Dict,
|
|
243
|
+
resource_type: str,
|
|
244
|
+
resource: str,
|
|
245
|
+
) -> Optional[str]:
|
|
246
|
+
"""Pick the template for a resource, preferring a "type/sub-type" key over the plain type.
|
|
247
|
+
|
|
248
|
+
Some ARNs carry their real kind one level down: apigateway "restapis/{id}/stages/{stage}" (sub-type in
|
|
249
|
+
the second segment) or wafv2 "regional/webacl/{name}/{id}" (sub-type in the first segment).
|
|
250
|
+
"""
|
|
251
|
+
parts = resource.split("/")
|
|
252
|
+
for sub_type in parts[1:2] + parts[:1] if len(parts) > 1 else []:
|
|
253
|
+
template = service_links.get(f"{resource_type}/{sub_type}")
|
|
254
|
+
if template:
|
|
255
|
+
return template
|
|
256
|
+
return service_links.get(resource_type)
|
|
180
257
|
|
|
181
258
|
|
|
182
259
|
__all__ = [
|
|
@@ -42,12 +42,15 @@ def get_links() -> Dict:
|
|
|
42
42
|
"apps": 'https://{data.get("region", "")}.{data.get("console", "")}/amplify/home?region={data.get("region", "")}#/{data.get("resource", "")}',
|
|
43
43
|
},
|
|
44
44
|
"apigateway": { # Manage Amazon API Gateway
|
|
45
|
-
"restapis": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/
|
|
46
|
-
|
|
45
|
+
"restapis": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/main/apis/\
|
|
46
|
+
{resource_part(0)}/resources?api={resource_part(0)}®ion={data.get("region", "")}',
|
|
47
|
+
"restapis/stages": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/main/apis/\
|
|
48
|
+
{resource_part(0)}/stages?api={resource_part(0)}®ion={data.get("region", "")}',
|
|
47
49
|
"stages": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/\
|
|
48
50
|
home?region={data.get("region", "")}#/{data.get("resource", "")}',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
# HTTP and WebSocket APIs (API Gateway v2)
|
|
52
|
+
"apis": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/main/api-detail?\
|
|
53
|
+
api={resource_part(0)}®ion={data.get("region", "")}',
|
|
51
54
|
"stage": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/\
|
|
52
55
|
home?region={data.get("region", "")}#/{data.get("resource", "")}',
|
|
53
56
|
"clientcertificates": 'https://{data.get("region", "")}.{data.get("console", "")}/apigateway/home?region={data.get("region", "")}#/client-certificates',
|
|
@@ -95,7 +98,7 @@ def get_links() -> Dict:
|
|
|
95
98
|
"workgroup": 'https://{data.get("region", "")}.{data.get("console", "")}/athena/home?region={data.get("region", "")}#/workgroups/details/{data.get("resource", "")}',
|
|
96
99
|
},
|
|
97
100
|
"autoscaling": { # Amazon EC2 Auto Scaling
|
|
98
|
-
"autoScalingGroup": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#AutoScalingGroupDetails:id={
|
|
101
|
+
"autoScalingGroup": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#AutoScalingGroupDetails:id={resource_tail}',
|
|
99
102
|
"launchConfiguration": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#LaunchConfigurations:',
|
|
100
103
|
},
|
|
101
104
|
"aws-marketplace": { # AWS Marketplace Catalog
|
|
@@ -238,6 +241,8 @@ def get_links() -> Dict:
|
|
|
238
241
|
"aggregation-authorization": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/aggregations',
|
|
239
242
|
"config-aggregator": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/aggregations',
|
|
240
243
|
"config-rule": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/rules/rule-details/{data.get("resource", "")}',
|
|
244
|
+
"configuration-recorder": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/settings',
|
|
245
|
+
"delivery-channel": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/settings',
|
|
241
246
|
"conformance-pack": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/conformance-packs/details/{data.get("resource", "")}',
|
|
242
247
|
"organization-config-rule": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/rules',
|
|
243
248
|
"organization-conformance-pack": 'https://{data.get("region", "")}.{data.get("console", "")}/config/home?region={data.get("region", "")}#/conformance-packs',
|
|
@@ -345,7 +350,7 @@ def get_links() -> Dict:
|
|
|
345
350
|
"table": 'https://{data.get("region", "")}.{data.get("console", "")}/dynamodbv2/home?region=\
|
|
346
351
|
{data.get("region", "")}#table?name={data.get("resource", "")}',
|
|
347
352
|
"secondary_indexes": 'https://{data.get("region", "")}.{data.get("console", "")}/dynamodbv2/\
|
|
348
|
-
home?region=
|
|
353
|
+
home?region={data.get("region", "")}#table?initialTagKey=&name={data.get("resource", "")}&tab=indexes',
|
|
349
354
|
},
|
|
350
355
|
"ec2": { # AWS Systems Manager
|
|
351
356
|
"capacity-reservation": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#CapacityReservationDetails:capacityReservationId={data.get("resource", "")}',
|
|
@@ -387,7 +392,7 @@ def get_links() -> Dict:
|
|
|
387
392
|
"traffic-mirror-session": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#TrafficMirrorSessions:',
|
|
388
393
|
"traffic-mirror-target": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#TrafficMirrorTargets:',
|
|
389
394
|
"transit-gateway": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#TransitGateways:transitGatewayId={data.get("resource", "")}',
|
|
390
|
-
"transit-gateway-attachment": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#
|
|
395
|
+
"transit-gateway-attachment": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#TransitGatewayAttachmentDetails:transitGatewayAttachmentId={data.get("resource", "")}',
|
|
391
396
|
"transit-gateway-multicast-domain": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#TransitGatewayMulticastDomains:',
|
|
392
397
|
"transit-gateway-route-table": 'https://{data.get("region", "")}.{data.get("console", "")}/vpcconsole/home?region={data.get("region", "")}#TransitGatewayRouteTables:',
|
|
393
398
|
"volume": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2\
|
|
@@ -414,19 +419,19 @@ def get_links() -> Dict:
|
|
|
414
419
|
{data.get("account", "")}/{data.get("resource", "")}?region={data.get("region", "")}',
|
|
415
420
|
},
|
|
416
421
|
"ecs": { # Amazon Elastic Container Service
|
|
417
|
-
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"task
|
|
422
|
-
|
|
422
|
+
# Resource paths are cluster/<id>; the classic #/clusters console is retired, so these use /ecs/v2.
|
|
423
|
+
"cluster": 'https://{data.get("region", "")}.{data.get("console", "")}/ecs/v2/clusters/{resource_part(0)}?region={data.get("region", "")}',
|
|
424
|
+
"container-instance": 'https://{data.get("region", "")}.{data.get("console", "")}/ecs/v2/clusters/{resource_part(0)}/infrastructure?region={data.get("region", "")}',
|
|
425
|
+
"service": 'https://{data.get("region", "")}.{data.get("console", "")}/ecs/v2/clusters/{resource_part(0)}/services/{resource_part(1)}?region={data.get("region", "")}',
|
|
426
|
+
"task": 'https://{data.get("region", "")}.{data.get("console", "")}/ecs/v2/clusters/{resource_part(0)}/tasks/{resource_part(1)}?region={data.get("region", "")}',
|
|
427
|
+
"task-definition": 'https://{data.get("region", "")}.{data.get("console", "")}/ecs/v2/task-definitions/{resource_part(0)}?region={data.get("region", "")}',
|
|
423
428
|
"task-set": None, # task-set is a sub-resource of service, no direct deep-link
|
|
424
429
|
},
|
|
425
430
|
"eks": { # Amazon Elastic Container Service for Kubernetes
|
|
426
431
|
"cluster": 'https://{data.get("region", "")}.{data.get("console", "")}/eks/home?region={data.get\
|
|
427
432
|
("region", "")}#/clusters/{data.get("resource", "")}',
|
|
428
433
|
"fargateprofile": 'https://{data.get("region", "")}.{data.get("console", "")}/eks/home?region={data.get("region", "")}#/clusters/{data.get("resource", "")}/fargate-profiles',
|
|
429
|
-
"nodegroup": 'https://{data.get("region", "")}.{data.get("console", "")}/eks/home?region={data.get("region", "")}#/clusters/{
|
|
434
|
+
"nodegroup": 'https://{data.get("region", "")}.{data.get("console", "")}/eks/home?region={data.get("region", "")}#/clusters/{resource_part(0)}/nodegroups/{resource_part(1)}',
|
|
430
435
|
},
|
|
431
436
|
"elastic-inference": { # Amazon Elastic Inference (retired April 2024)
|
|
432
437
|
"elastic-inference-accelerator": None, # service retired
|
|
@@ -456,10 +461,20 @@ def get_links() -> Dict:
|
|
|
456
461
|
"file-system": 'https://{data.get("region", "")}.{data.get("console", "")}/efs/home?region={data.get("region", "")}#/file-systems/{data.get("resource", "")}',
|
|
457
462
|
},
|
|
458
463
|
"elasticloadbalancing": { # AWS WAF V2
|
|
459
|
-
|
|
464
|
+
# A listener (listener/app/<lb>/<lb-id>/<id>) links to its load balancer's page.
|
|
465
|
+
"listener": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}\
|
|
466
|
+
#LoadBalancer:loadBalancerArn=arn:{partition}:elasticloadbalancing:{data.get("region", "")}:\
|
|
467
|
+
{data.get("account", "")}:loadbalancer/{resource_part(0)}/{resource_part(1)}/{resource_part(2)}',
|
|
460
468
|
"listener-rule": None, # sub-resource of listener, no direct deep-link
|
|
469
|
+
# Classic load balancers (loadbalancer/<name>) have no ARN-addressable page.
|
|
461
470
|
"loadbalancer": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/\
|
|
462
471
|
home?region={data.get("region", "")}#LoadBalancers:sort=loadBalancerName',
|
|
472
|
+
"loadbalancer/app": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region=\
|
|
473
|
+
{data.get("region", "")}#LoadBalancer:loadBalancerArn={arn}',
|
|
474
|
+
"loadbalancer/net": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region=\
|
|
475
|
+
{data.get("region", "")}#LoadBalancer:loadBalancerArn={arn}',
|
|
476
|
+
"loadbalancer/gwy": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region=\
|
|
477
|
+
{data.get("region", "")}#LoadBalancer:loadBalancerArn={arn}',
|
|
463
478
|
"targetgroup": 'https://{data.get("region", "")}.{data.get("console", "")}/ec2/home?region={data.get("region", "")}#TargetGroups:targetGroupArn={arn}',
|
|
464
479
|
},
|
|
465
480
|
"elasticmapreduce": { # Amazon Elastic MapReduce
|
|
@@ -484,6 +499,8 @@ def get_links() -> Dict:
|
|
|
484
499
|
home?region={data.get("region", "")}#opensearch/domains/{data.get("resource", "")}',
|
|
485
500
|
"reserved_instance": 'https://{data.get("region", "")}.{data.get("console", "")}/esv3/\
|
|
486
501
|
home?region={data.get("region", "")}#opensearch/reserved-instances',
|
|
502
|
+
"reserved-instances": 'https://{data.get("region", "")}.{data.get("console", "")}/esv3/\
|
|
503
|
+
home?region={data.get("region", "")}#opensearch/reserved-instances',
|
|
487
504
|
},
|
|
488
505
|
"events": { # Amazon EventBridge
|
|
489
506
|
"event-bus": 'https://{data.get("region", "")}.{data.get("console", "")}/events/home?region={data.get("region", "")}#/eventbus/{data.get("resource", "")}',
|
|
@@ -585,7 +602,7 @@ def get_links() -> Dict:
|
|
|
585
602
|
"saml-provider": 'https://{data.get("console", "")}/iamv2/home#/identity-providers',
|
|
586
603
|
"server-certificate": 'https://{data.get("console", "")}/iam/home?#/server-certificates',
|
|
587
604
|
"sms-mfa": None, # no direct console deep-link for SMS MFA device
|
|
588
|
-
"user": 'https://{data.get("console", "")}/iam/home?#/users/{data.get("resource", "")}',
|
|
605
|
+
"user": 'https://{data.get("console", "")}/iam/home?#/users/{get_resource_path(data.get("resource", ""))}',
|
|
589
606
|
"access_keys": 'https://{data.get("console", "")}/iam/home#/users/{data.get("resource", "")}?section=security_credentials',
|
|
590
607
|
"policy_statement": 'https://{data.get("console", "")}/iam/home?#/policies/{get_arn_string(data)}',
|
|
591
608
|
},
|
|
@@ -729,7 +746,7 @@ def get_links() -> Dict:
|
|
|
729
746
|
"StaticIp": "https://lightsail.aws.amazon.com/ls/webapp/home/static-ips",
|
|
730
747
|
},
|
|
731
748
|
"logs": { # Amazon CloudWatch Logs
|
|
732
|
-
"log-group": 'https://{data.get("region", "")}.{data.get("console", "")}/cloudwatch/home?region={data.get("region", "")}#logsV2:log-groups/log-group/{
|
|
749
|
+
"log-group": 'https://{data.get("region", "")}.{data.get("console", "")}/cloudwatch/home?region={data.get("region", "")}#logsV2:log-groups/log-group/{log_group_id}',
|
|
733
750
|
},
|
|
734
751
|
"lookoutequipment": { # Amazon Lookout for Equipment
|
|
735
752
|
"dataset": 'https://{data.get("region", "")}.{data.get("console", "")}/lookoutequipment/home?region={data.get("region", "")}#/datasets/{data.get("resource", "")}',
|
|
@@ -850,7 +867,7 @@ def get_links() -> Dict:
|
|
|
850
867
|
"organizations": { # AWS Organizations
|
|
851
868
|
"account": 'https://{data.get("console", "")}/organizations/v2/home/accounts/{data.get("resource", "")}',
|
|
852
869
|
"handshake": None, # no direct console deep-link for handshakes
|
|
853
|
-
"organization": 'https://{data.get("
|
|
870
|
+
"organization": 'https://{data.get("console", "")}/organizations/v2/home',
|
|
854
871
|
"ou": 'https://{data.get("console", "")}/organizations/v2/home/roots',
|
|
855
872
|
"policy": 'https://{data.get("console", "")}/organizations/v2/home/policies/{data.get("resource", "")}',
|
|
856
873
|
"root": 'https://{data.get("console", "")}/organizations/v2/home/roots',
|
|
@@ -923,8 +940,8 @@ def get_links() -> Dict:
|
|
|
923
940
|
"ri": 'https://{data.get("region", "")}.{data.get("console", "")}/rds/home?region={data.get("region", "")}#reserved-instances:',
|
|
924
941
|
"secgrp": 'https://{data.get("region", "")}.{data.get("console", "")}/vpc/home?region=\
|
|
925
942
|
{data.get("region", "")}#SecurityGroup:groupId={data.get("resource", "")}',
|
|
926
|
-
"snapshot": 'https://{data.get("region", "")}.{data.get("console", "")}/rds/home?region={data.get("region", "")}#db-snapshot:
|
|
927
|
-
"subgrp": 'https://{data.get("region", "")}.{data.get("console", "")}/rds/home?region={data.get("region", "")}#subnet-
|
|
943
|
+
"snapshot": 'https://{data.get("region", "")}.{data.get("console", "")}/rds/home?region={data.get("region", "")}#db-snapshot:id={data.get("resource", "")}',
|
|
944
|
+
"subgrp": 'https://{data.get("region", "")}.{data.get("console", "")}/rds/home?region={data.get("region", "")}#db-subnet-group:id={data.get("resource", "")}',
|
|
928
945
|
"target": None, # sub-resource of db-proxy, no direct deep-link
|
|
929
946
|
"target-group": None, # sub-resource of db-proxy, no direct deep-link
|
|
930
947
|
},
|
|
@@ -976,14 +993,16 @@ def get_links() -> Dict:
|
|
|
976
993
|
"change": None, # internal resource, no direct console page
|
|
977
994
|
"delegationset": 'https://{data.get("console", "")}/route53/home#/hosted-zones',
|
|
978
995
|
"healthcheck": 'https://{data.get("console", "")}/route53/healthchecks/home',
|
|
979
|
-
"hostedzone"
|
|
980
|
-
{data.get("
|
|
996
|
+
# get_resource_path also accepts the "hostedzone//hostedzone/Z1" ARNs built from the API's zone Id.
|
|
997
|
+
"hostedzone": 'https://{data.get("console", "")}/route53/v2/hostedzones#ListRecordSets/\
|
|
998
|
+
{get_resource_path(data.get("resource", ""))}',
|
|
981
999
|
"queryloggingconfig": 'https://{data.get("console", "")}/route53/home#/query-logging',
|
|
982
1000
|
"trafficpolicy": 'https://{data.get("console", "")}/route53/trafficflow/home#/policy/\
|
|
983
1001
|
{data.get("resource", "")}',
|
|
984
1002
|
"trafficpolicyinstance": 'https://{data.get("console", "")}/route53/trafficflow/home#/\
|
|
985
1003
|
modify-records/edit/{data.get("resource", "")}',
|
|
986
|
-
|
|
1004
|
+
# Route 53 Domains is global: its ARNs carry no region, so the host has none.
|
|
1005
|
+
"domains": 'https://{data.get("console", "")}/route53/home#DomainListing:',
|
|
987
1006
|
},
|
|
988
1007
|
"route53resolver": { # Amazon Route 53 Resolver
|
|
989
1008
|
"resolver-endpoint": 'https://{data.get("region", "")}.{data.get("console", "")}/route53resolver/home?region={data.get("region", "")}#/endpoints/{data.get("resource", "")}',
|
|
@@ -1105,8 +1124,9 @@ def get_links() -> Dict:
|
|
|
1105
1124
|
"windowtask": 'https://{data.get("region", "")}.{data.get("console", "")}/systems-manager/maintenance-windows?region={data.get("region", "")}',
|
|
1106
1125
|
},
|
|
1107
1126
|
"sso": { # AWS IAM Identity Center
|
|
1127
|
+
# Identity Center ARNs carry no region; the console opens the instance's home region.
|
|
1108
1128
|
"instance": 'https://{data.get("region", "")}.{data.get("console", "")}/singlesignon/home?region={data.get("region", "")}',
|
|
1109
|
-
"permissionSet": 'https://{data.get("region", "")}.{data.get("console", "")}/singlesignon/home?region={data.get("region", "")}
|
|
1129
|
+
"permissionSet": 'https://{data.get("region", "")}.{data.get("console", "")}/singlesignon/home?region={data.get("region", "")}',
|
|
1110
1130
|
},
|
|
1111
1131
|
"states": { # AWS Step Functions
|
|
1112
1132
|
"activity": 'https://{data.get("region", "")}.{data.get("console", "")}/states/home?region={data.get("region", "")}#/activities/{arn}',
|
|
@@ -1177,6 +1197,11 @@ def get_links() -> Dict:
|
|
|
1177
1197
|
"xssmatchset": None, # WAF Classic condition, no dedicated console deep-link
|
|
1178
1198
|
},
|
|
1179
1199
|
"wafv2": { # AWS WAF V2
|
|
1200
|
+
# Web ACL ARNs: {regional|global}/webacl/<name>/<id>; global (CloudFront) ACLs use region=global.
|
|
1201
|
+
"regional/webacl": 'https://{data.get("region", "")}.{data.get("console", "")}/wafv2/homev2/web-acl/\
|
|
1202
|
+
{resource_part(1)}/{resource_part(2)}/overview?region={data.get("region", "")}',
|
|
1203
|
+
"global/webacl": 'https://{data.get("region", "")}.{data.get("console", "")}/wafv2/homev2/web-acl/\
|
|
1204
|
+
{resource_part(1)}/{resource_part(2)}/overview?region=global',
|
|
1180
1205
|
},
|
|
1181
1206
|
"wellarchitected": { # AWS Well-Architected Tool
|
|
1182
1207
|
"workload": 'https://{data.get("region", "")}.{data.get("console", "")}/wellarchitected/home?region={data.get("region", "")}#/workload/{data.get("resource", "")}',
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
PORTAL = "https://portal.azure.com"
|
|
9
|
+
SUBSCRIPTIONS_BLADE = f"{PORTAL}/#view/Microsoft_Azure_Billing/SubscriptionsBlade"
|
|
10
|
+
ACTIVITY_LOG_BLADE = f"{PORTAL}/#view/Microsoft_Azure_Monitoring/AzureMonitoringBrowseBlade/~/activityLog"
|
|
11
|
+
|
|
12
|
+
_GUID_RE = re.compile(r"^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$")
|
|
13
|
+
_PROVIDERS_RE = re.compile(r"/providers/", re.IGNORECASE)
|
|
14
|
+
_AUTHORIZATION_RE = re.compile(r"/providers/microsoft\.authorization/", re.IGNORECASE)
|
|
15
|
+
|
|
16
|
+
# Nested ARM types the portal opens as resources in their own right, so a link keeps
|
|
17
|
+
# drilling into them instead of stopping at their parent. Keys are lowercase type paths.
|
|
18
|
+
FIRST_CLASS_CHILDREN = {
|
|
19
|
+
"microsoft.sql/servers/databases",
|
|
20
|
+
"microsoft.sql/servers/elasticpools",
|
|
21
|
+
"microsoft.sql/managedinstances/databases",
|
|
22
|
+
"microsoft.web/sites/slots",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Nested ARM types with no page of their own: link to the parent blade that lists them.
|
|
26
|
+
# Anything nested and not listed here links to the parent's overview.
|
|
27
|
+
CHILD_BLADES = {
|
|
28
|
+
"microsoft.network/virtualnetworks/subnets": "subnets",
|
|
29
|
+
"microsoft.network/routetables/routes": "routes",
|
|
30
|
+
"microsoft.network/networksecuritygroups/securityrules": "inboundSecurityRules",
|
|
31
|
+
"microsoft.network/networksecuritygroups/defaultsecurityrules": "inboundSecurityRules",
|
|
32
|
+
"microsoft.containerservice/managedclusters/agentpools": "nodePools",
|
|
33
|
+
"microsoft.storage/storageaccounts/blobservices": "containersList",
|
|
34
|
+
"microsoft.storage/storageaccounts/fileservices": "fileList",
|
|
35
|
+
"microsoft.storage/storageaccounts/queueservices": "queueList",
|
|
36
|
+
"microsoft.storage/storageaccounts/tableservices": "tableList",
|
|
37
|
+
"microsoft.compute/virtualmachines/extensions": "extensions",
|
|
38
|
+
"microsoft.compute/virtualmachinescalesets/extensions": "extensions",
|
|
39
|
+
"microsoft.documentdb/databaseaccounts/sqldatabases": "dataExplorer",
|
|
40
|
+
"microsoft.documentdb/databaseaccounts/mongodbdatabases": "dataExplorer",
|
|
41
|
+
"microsoft.documentdb/databaseaccounts/cassandrakeyspaces": "dataExplorer",
|
|
42
|
+
"microsoft.documentdb/databaseaccounts/gremlindatabases": "dataExplorer",
|
|
43
|
+
"microsoft.documentdb/databaseaccounts/tables": "dataExplorer",
|
|
44
|
+
"microsoft.web/sites/config": "configuration",
|
|
45
|
+
"microsoft.web/sites/slots/config": "configuration",
|
|
46
|
+
"microsoft.containerregistry/registries/replications": "replications",
|
|
47
|
+
"microsoft.containerregistry/registries/webhooks": "webhooks",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Provider types with no portal page at all: link to the scope they live under.
|
|
51
|
+
SCOPE_ONLY_TYPES = {
|
|
52
|
+
"microsoft.security/securitycontacts",
|
|
53
|
+
"microsoft.network/locations",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _anchor_and_blade(segments: list) -> Tuple[str, str]:
|
|
58
|
+
"""Split `ns/type/name[/childType/childName...]` into the linkable resource and its blade."""
|
|
59
|
+
type_path = "/".join(segments[:2]).lower()
|
|
60
|
+
end = 3
|
|
61
|
+
while (
|
|
62
|
+
len(segments) > end
|
|
63
|
+
and f"{type_path}/{segments[end].lower()}" in FIRST_CLASS_CHILDREN
|
|
64
|
+
):
|
|
65
|
+
type_path = f"{type_path}/{segments[end].lower()}"
|
|
66
|
+
end += 2
|
|
67
|
+
if len(segments) <= end:
|
|
68
|
+
return "/".join(segments), "overview"
|
|
69
|
+
return "/".join(segments[:end]), CHILD_BLADES.get(
|
|
70
|
+
f"{type_path}/{segments[end].lower()}",
|
|
71
|
+
"overview",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _management_link(arm_id: str, domain: str) -> str:
|
|
76
|
+
base = f"{PORTAL}/#@{domain}/resource"
|
|
77
|
+
if _GUID_RE.match(arm_id): # bare subscription id
|
|
78
|
+
return f"{base}/subscriptions/{arm_id}/overview"
|
|
79
|
+
|
|
80
|
+
authorization = _AUTHORIZATION_RE.search(arm_id)
|
|
81
|
+
if (
|
|
82
|
+
authorization
|
|
83
|
+
): # role definitions/assignments: the scope's Access control (IAM) blade
|
|
84
|
+
scope = arm_id[: authorization.start()]
|
|
85
|
+
return f"{base}{scope}/users" if scope else SUBSCRIPTIONS_BLADE
|
|
86
|
+
|
|
87
|
+
parts = _PROVIDERS_RE.split(arm_id, maxsplit=1)
|
|
88
|
+
if len(parts) == 1: # subscription or resource group
|
|
89
|
+
return f"{base}{arm_id}/overview"
|
|
90
|
+
|
|
91
|
+
scope, tail = parts
|
|
92
|
+
segments = tail.split("/")
|
|
93
|
+
type_path = "/".join(segments[:2]).lower()
|
|
94
|
+
if type_path == "microsoft.insights/logprofiles":
|
|
95
|
+
return ACTIVITY_LOG_BLADE
|
|
96
|
+
if type_path in SCOPE_ONLY_TYPES:
|
|
97
|
+
return f"{base}{scope}/overview" if scope else SUBSCRIPTIONS_BLADE
|
|
98
|
+
|
|
99
|
+
anchor, blade = _anchor_and_blade(segments)
|
|
100
|
+
return f"{base}{scope}/providers/{anchor}/{blade}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _key_vault_link(uri: str, domain: str) -> str:
|
|
104
|
+
for marker, kind in (
|
|
105
|
+
("/keys/", "Key"),
|
|
106
|
+
("/secrets/", "Secret"),
|
|
107
|
+
("/certificates/", "Certificate"),
|
|
108
|
+
):
|
|
109
|
+
if marker in uri:
|
|
110
|
+
return f"{PORTAL}/#@{domain}/asset/Microsoft_Azure_KeyVault/{kind}/{uri}"
|
|
111
|
+
return ""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AzureLinker:
|
|
115
|
+
def get_console_link(
|
|
116
|
+
self,
|
|
117
|
+
id: str,
|
|
118
|
+
app_id: str = "",
|
|
119
|
+
primary_ad_domain_name: str = "",
|
|
120
|
+
iam_entity_type: str = "",
|
|
121
|
+
) -> str:
|
|
122
|
+
if not primary_ad_domain_name and not iam_entity_type:
|
|
123
|
+
logger.debug(
|
|
124
|
+
"For IAM entity required `iam_entity_type` and for other entity required `primary_ad_domain_name`",
|
|
125
|
+
)
|
|
126
|
+
raise ValueError("Invalid parameters provided")
|
|
127
|
+
iam_entities = {
|
|
128
|
+
"user": "https://portal.azure.com/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/Profile/userId/{id}",
|
|
129
|
+
"group": "https://portal.azure.com/#blade/Microsoft_AAD_IAM/GroupDetailsMenuBlade/Overview/groupId/{id}",
|
|
130
|
+
"application": "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade\
|
|
131
|
+
/Overview/appId/{id}",
|
|
132
|
+
"role": "https://portal.azure.com/#@{primary_ad_domain_name}/resource{id}/roles",
|
|
133
|
+
"service_principal": "https://portal.azure.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Properties/\
|
|
134
|
+
objectId/{id}/appId/{app_id}/preferredSingleSignOnMode~/null",
|
|
135
|
+
"domain": "https://portal.azure.com/#view/Microsoft_AAD_IAM/DomainPropertiesBladeV2/domainName/{id}",
|
|
136
|
+
"tenant": "https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if iam_entity_type and id and iam_entities.get(iam_entity_type):
|
|
140
|
+
return (
|
|
141
|
+
iam_entities[iam_entity_type]
|
|
142
|
+
.format(
|
|
143
|
+
id=id,
|
|
144
|
+
primary_ad_domain_name=primary_ad_domain_name,
|
|
145
|
+
app_id=app_id,
|
|
146
|
+
)
|
|
147
|
+
.replace(" ", "")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
elif primary_ad_domain_name and id:
|
|
151
|
+
if id.startswith("https"):
|
|
152
|
+
key_vault_link = _key_vault_link(id, primary_ad_domain_name)
|
|
153
|
+
if key_vault_link:
|
|
154
|
+
return key_vault_link
|
|
155
|
+
|
|
156
|
+
return _management_link(id, primary_ad_domain_name)
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
logger.debug("entity id required")
|
|
160
|
+
raise ValueError("Invalid parameters provided")
|