|
1 | 1 | import json |
| 2 | +import os |
2 | 3 | import secrets |
3 | 4 | from datetime import datetime, timedelta |
4 | 5 | from urllib.parse import urlparse |
5 | 6 |
|
| 7 | +import requests |
| 8 | +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential |
6 | 9 | from django.conf import settings |
7 | 10 | from django.contrib.auth import authenticate, login, logout |
8 | 11 | from django.contrib.auth.models import User |
@@ -1068,32 +1071,109 @@ def logout_user(request): |
1068 | 1071 | return redirect(reverse(settings.LOGIN_URL)) |
1069 | 1072 |
|
1070 | 1073 |
|
| 1074 | +# Power BI embedding via managed identity |
| 1075 | +PBI_SCOPE = "https://analysis.windows.net/powerbi/api/.default" |
| 1076 | +PBI_BASE = "https://api.powerbi.com/v1.0/ifrc" # myorg? |
| 1077 | + |
| 1078 | + |
| 1079 | +def _pbi_token_via_managed_identity() -> str | None: |
| 1080 | + """ |
| 1081 | + Acquire an AAD access token for Power BI using the AKS managed identity. |
| 1082 | + If AZURE_CLIENT_ID is provided, target that user-assigned MI. |
| 1083 | + """ |
| 1084 | + try: |
| 1085 | + client_id = getattr(settings, "AZURE_CLIENT_ID", None) or os.getenv("AZURE_CLIENT_ID") |
| 1086 | + if client_id: |
| 1087 | + cred = ManagedIdentityCredential(client_id=client_id) |
| 1088 | + else: |
| 1089 | + # Will use workload identity / MSI / Azure CLI (in dev) automatically |
| 1090 | + cred = DefaultAzureCredential(exclude_interactive_browser_credential=True) |
| 1091 | + return cred.get_token(PBI_SCOPE).token |
| 1092 | + except Exception as exc: |
| 1093 | + logger.exception("Power BI MI token acquisition failed: %s", exc) |
| 1094 | + return None |
| 1095 | + |
| 1096 | + |
| 1097 | +def _pbi_get_embed_info(access_token: str, workspace_id: str, report_id: str | None = None, timeout: int = 10): |
| 1098 | + """ |
| 1099 | + Get report embedUrl and generate an embed token (embed-for-your-organization). |
| 1100 | + Requires the service principal (managed identity) to be a member of the workspace. |
| 1101 | + """ |
| 1102 | + headers = {"Authorization": f"Bearer {access_token}"} |
| 1103 | + |
| 1104 | + # Resolve report metadata |
| 1105 | + if report_id: |
| 1106 | + r = requests.get(f"{PBI_BASE}/groups/{workspace_id}/reports/{report_id}", headers=headers, timeout=timeout) |
| 1107 | + r.raise_for_status() |
| 1108 | + rep = r.json() |
| 1109 | + else: |
| 1110 | + r = requests.get(f"{PBI_BASE}/groups/{workspace_id}/reports", headers=headers, timeout=timeout) |
| 1111 | + r.raise_for_status() |
| 1112 | + items = r.json().get("value", []) |
| 1113 | + if not items: |
| 1114 | + raise RuntimeError("No reports found in workspace.") |
| 1115 | + rep = items[0] |
| 1116 | + report_id = rep["id"] |
| 1117 | + |
| 1118 | + embed_url = rep["embedUrl"] |
| 1119 | + |
| 1120 | + # Generate embed token (view access) |
| 1121 | + payload = {"accessLevel": "View"} |
| 1122 | + t = requests.post( |
| 1123 | + f"{PBI_BASE}/groups/{workspace_id}/reports/{report_id}/GenerateToken", |
| 1124 | + headers={**headers, "Content-Type": "application/json"}, |
| 1125 | + json=payload, |
| 1126 | + timeout=timeout, |
| 1127 | + ) |
| 1128 | + t.raise_for_status() |
| 1129 | + token_json = t.json() |
| 1130 | + embed_token = token_json["token"] |
| 1131 | + expires = token_json.get("expiration") |
| 1132 | + |
| 1133 | + return embed_url, report_id, embed_token, expires |
| 1134 | + |
| 1135 | + |
1071 | 1136 | class AuthPowerBI(APIView): |
1072 | 1137 | authentication_classes = (authentication.SessionAuthentication,) |
1073 | 1138 | permission_classes = (permissions.IsAuthenticated,) |
1074 | 1139 |
|
1075 | 1140 | def get(self, request): |
1076 | | - # Temporary mock values until Power BI REST integration is added |
| 1141 | + # Try real Power BI via managed identity |
| 1142 | + workspace_id = getattr( |
| 1143 | + settings, "POWERBI_WORKSPACE_ID", "ac23af44-f635-40e4-9091-5e6ba88bdaf3" |
| 1144 | + ) # Should be no default, FIXME |
| 1145 | + report_id_cfg = getattr(settings, "POWERBI_REPORT_ID", None) # ? 029be7d5-36c6-496e-a613-8fcf051f4ed6 |
| 1146 | + access_token = _pbi_token_via_managed_identity() |
| 1147 | + |
| 1148 | + if access_token and workspace_id: |
| 1149 | + try: |
| 1150 | + embed_url, report_id, embed_token, expires_at = _pbi_get_embed_info(access_token, workspace_id, report_id_cfg) |
| 1151 | + return Response( |
| 1152 | + { |
| 1153 | + "detail": "ok", |
| 1154 | + "embed_url": embed_url, |
| 1155 | + "report_id": report_id, |
| 1156 | + "embed_token": embed_token, |
| 1157 | + "expires_at": expires_at, |
| 1158 | + "user": request.user.username, |
| 1159 | + } |
| 1160 | + ) |
| 1161 | + except Exception as e: |
| 1162 | + logger.exception("Power BI REST call failed, falling back to mock: %s", e) |
| 1163 | + |
| 1164 | + # Fallback mock if not configured or failed |
1077 | 1165 | embed_token = secrets.token_hex(8) # 16-char hex |
| 1166 | + embed_url = get_random_string(10) |
| 1167 | + report_id = secrets.randbelow(2_147_483_647) + 1 |
1078 | 1168 | expires_at = (timezone.now() + timedelta(hours=1)).isoformat() |
1079 | | - group_id = get_random_string(36) # UUID-like random slug – workspace id |
1080 | | - group_id = "ac23af44-f635-40e4-9091-5e6ba88bdaf3" # fixed for testing |
1081 | | - report_id = get_random_string(36) # UUID-like random slug |
1082 | | - report_id = "029be7d5-36c6-496e-a613-8fcf051f4ed6" # fixed for testing |
1083 | | - report_section = get_random_string(16) # default section |
1084 | | - report_section = "067c44735dc44119482e" # fixed for testing |
1085 | | - # embed_url = f"https://app.powerbi.com/reportEmbed?reportId={report_id}&groupId={group_id}" |
1086 | | - embed_url = f"https://app.powerbi.com/groups/{group_id}/reports/{report_id}/{report_section}" |
1087 | 1169 |
|
1088 | 1170 | return Response( |
1089 | 1171 | { |
1090 | 1172 | "detail": "ok", |
1091 | | - "embed_token": embed_token, |
1092 | 1173 | "embed_url": embed_url, |
1093 | | - "expires_at": expires_at, |
1094 | | - "group_id": group_id, |
1095 | 1174 | "report_id": report_id, |
1096 | | - "report_section": report_section, |
| 1175 | + "embed_token": embed_token, |
| 1176 | + "expires_at": expires_at, |
1097 | 1177 | "user": request.user.username, |
1098 | 1178 | } |
1099 | 1179 | ) |
0 commit comments