|  | 
| 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,110 @@ 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 | +        client_id = "8876ce49-0952-4fc5-b648-b1695bd03ebd"  # Staging, Prod: "f1d59a17-c79e-4f18-b444-5eb95730868f" | 
|  | 1087 | +        if client_id: | 
|  | 1088 | +            cred = ManagedIdentityCredential(client_id=client_id) | 
|  | 1089 | +        else: | 
|  | 1090 | +            # Will use workload identity / MSI / Azure CLI (in dev) automatically | 
|  | 1091 | +            cred = DefaultAzureCredential(exclude_interactive_browser_credential=True) | 
|  | 1092 | +        return cred.get_token(PBI_SCOPE).token | 
|  | 1093 | +    except Exception as exc: | 
|  | 1094 | +        logger.exception("Power BI MI token acquisition failed: %s", exc) | 
|  | 1095 | +        return None | 
|  | 1096 | + | 
|  | 1097 | + | 
|  | 1098 | +def _pbi_get_embed_info(access_token: str, workspace_id: str, report_id: str | None = None, timeout: int = 10): | 
|  | 1099 | +    """ | 
|  | 1100 | +    Get report embedUrl and generate an embed token (embed-for-your-organization). | 
|  | 1101 | +    Requires the service principal (managed identity) to be a member of the workspace. | 
|  | 1102 | +    """ | 
|  | 1103 | +    headers = {"Authorization": f"Bearer {access_token}"} | 
|  | 1104 | + | 
|  | 1105 | +    # Resolve report metadata | 
|  | 1106 | +    if report_id: | 
|  | 1107 | +        r = requests.get(f"{PBI_BASE}/groups/{workspace_id}/reports/{report_id}", headers=headers, timeout=timeout) | 
|  | 1108 | +        r.raise_for_status() | 
|  | 1109 | +        rep = r.json() | 
|  | 1110 | +    else: | 
|  | 1111 | +        r = requests.get(f"{PBI_BASE}/groups/{workspace_id}/reports", headers=headers, timeout=timeout) | 
|  | 1112 | +        r.raise_for_status() | 
|  | 1113 | +        items = r.json().get("value", []) | 
|  | 1114 | +        if not items: | 
|  | 1115 | +            raise RuntimeError("No reports found in workspace.") | 
|  | 1116 | +        rep = items[0] | 
|  | 1117 | +        report_id = rep["id"] | 
|  | 1118 | + | 
|  | 1119 | +    embed_url = rep["embedUrl"] | 
|  | 1120 | + | 
|  | 1121 | +    # Generate embed token (view access) | 
|  | 1122 | +    payload = {"accessLevel": "View"} | 
|  | 1123 | +    t = requests.post( | 
|  | 1124 | +        f"{PBI_BASE}/groups/{workspace_id}/reports/{report_id}/GenerateToken", | 
|  | 1125 | +        headers={**headers, "Content-Type": "application/json"}, | 
|  | 1126 | +        json=payload, | 
|  | 1127 | +        timeout=timeout, | 
|  | 1128 | +    ) | 
|  | 1129 | +    t.raise_for_status() | 
|  | 1130 | +    token_json = t.json() | 
|  | 1131 | +    embed_token = token_json["token"] | 
|  | 1132 | +    expires = token_json.get("expiration") | 
|  | 1133 | + | 
|  | 1134 | +    return embed_url, report_id, embed_token, expires | 
|  | 1135 | + | 
|  | 1136 | + | 
| 1071 | 1137 | class AuthPowerBI(APIView): | 
| 1072 | 1138 |     authentication_classes = (authentication.SessionAuthentication,) | 
| 1073 | 1139 |     permission_classes = (permissions.IsAuthenticated,) | 
| 1074 | 1140 | 
 | 
| 1075 | 1141 |     def get(self, request): | 
| 1076 |  | -        # Temporary mock values until Power BI REST integration is added | 
|  | 1142 | +        # Try real Power BI via managed identity | 
|  | 1143 | +        workspace_id = getattr( | 
|  | 1144 | +            settings, "POWERBI_WORKSPACE_ID", "ac23af44-f635-40e4-9091-5e6ba88bdaf3" | 
|  | 1145 | +        )  # Should be no default, FIXME | 
|  | 1146 | +        report_id_cfg = getattr(settings, "POWERBI_REPORT_ID", None)  # ? 029be7d5-36c6-496e-a613-8fcf051f4ed6 | 
|  | 1147 | +        access_token = _pbi_token_via_managed_identity() | 
|  | 1148 | + | 
|  | 1149 | +        if access_token and workspace_id: | 
|  | 1150 | +            try: | 
|  | 1151 | +                embed_url, report_id, embed_token, expires_at = _pbi_get_embed_info(access_token, workspace_id, report_id_cfg) | 
|  | 1152 | +                return Response( | 
|  | 1153 | +                    { | 
|  | 1154 | +                        "detail": "ok", | 
|  | 1155 | +                        "embed_url": embed_url, | 
|  | 1156 | +                        "report_id": report_id, | 
|  | 1157 | +                        "embed_token": embed_token, | 
|  | 1158 | +                        "expires_at": expires_at, | 
|  | 1159 | +                        "user": request.user.username, | 
|  | 1160 | +                    } | 
|  | 1161 | +                ) | 
|  | 1162 | +            except Exception as e: | 
|  | 1163 | +                logger.exception("Power BI REST call failed, falling back to mock: %s", e) | 
|  | 1164 | + | 
|  | 1165 | +        # Fallback mock if not configured or failed | 
| 1077 | 1166 |         embed_token = secrets.token_hex(8)  # 16-char hex | 
|  | 1167 | +        embed_url = get_random_string(10) | 
|  | 1168 | +        report_id = secrets.randbelow(2_147_483_647) + 1 | 
| 1078 | 1169 |         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 | 1170 | 
 | 
| 1088 | 1171 |         return Response( | 
| 1089 | 1172 |             { | 
| 1090 | 1173 |                 "detail": "ok", | 
| 1091 |  | -                "embed_token": embed_token, | 
| 1092 | 1174 |                 "embed_url": embed_url, | 
| 1093 |  | -                "expires_at": expires_at, | 
| 1094 |  | -                "group_id": group_id, | 
| 1095 | 1175 |                 "report_id": report_id, | 
| 1096 |  | -                "report_section": report_section, | 
|  | 1176 | +                "embed_token": embed_token, | 
|  | 1177 | +                "expires_at": expires_at, | 
| 1097 | 1178 |                 "user": request.user.username, | 
| 1098 | 1179 |             } | 
| 1099 | 1180 |         ) | 
0 commit comments