Skip to content

Commit 6c3de1d

Browse files
committed
AuthPowerBI – v0.2
1 parent f92fa94 commit 6c3de1d

File tree

2 files changed

+149
-13
lines changed

2 files changed

+149
-13
lines changed

api/test_views.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import re
12
import uuid
3+
from unittest.mock import patch
24

35
from django.contrib.auth.models import User
46
from django.core.files.uploadedfile import SimpleUploadedFile
7+
from django.test import override_settings
58
from django.urls import reverse
69

710
import api.models as models
@@ -37,6 +40,59 @@ def test_authenticated_returns_ok(self):
3740
self.assertEqual(data.get("detail"), "ok")
3841
self.assertEqual(data.get("user"), user.username)
3942

43+
def test_authenticated_returns_mock_values_shape(self):
44+
user = User.objects.create_user(username="bob", password="pass1234")
45+
self.assertEqual("bob", user.username)
46+
self.client.login(username="bob", password="pass1234")
47+
48+
# Force mock path regardless of settings by returning no MI token
49+
with patch("api.views._pbi_token_via_managed_identity", return_value=None):
50+
resp = self.client.get(self.url)
51+
self.assertEqual(resp.status_code, 200)
52+
data = resp.json()
53+
54+
# embed_token: 16 hex chars
55+
self.assertIsInstance(data.get("embed_token"), str)
56+
self.assertTrue(re.fullmatch(r"[0-9a-f]{16}", data["embed_token"]))
57+
# embed_url: 10-char random string
58+
self.assertIsInstance(data.get("embed_url"), str)
59+
self.assertEqual(len(data["embed_url"]), 10)
60+
# report_id: positive int
61+
self.assertIsInstance(data.get("report_id"), int)
62+
self.assertGreater(data["report_id"], 0)
63+
64+
def test_authenticated_powerbi_values_when_configured(self):
65+
user = User.objects.create_user(username="carol", password="pass1234")
66+
self.assertEqual("carol", user.username)
67+
self.client.login(username="carol", password="pass1234")
68+
69+
expected = {
70+
"embed_url": "https://app.powerbi.com/reportEmbed?reportId=rep-123",
71+
"report_id": "rep-123",
72+
"embed_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", # dummy string
73+
"expires_at": "2099-01-01T00:00:00Z",
74+
}
75+
76+
with override_settings(POWERBI_WORKSPACE_ID="ws-abc"):
77+
with (
78+
patch("api.views._pbi_token_via_managed_identity", return_value="access-token") as p_token,
79+
patch(
80+
"api.views._pbi_get_embed_info",
81+
return_value=(expected["embed_url"], expected["report_id"], expected["embed_token"], expected["expires_at"]),
82+
) as p_info,
83+
):
84+
resp = self.client.get(self.url)
85+
86+
self.assertEqual(resp.status_code, 200)
87+
data = resp.json()
88+
self.assertEqual(data.get("embed_url"), expected["embed_url"])
89+
self.assertEqual(data.get("report_id"), expected["report_id"])
90+
self.assertEqual(data.get("embed_token"), expected["embed_token"])
91+
self.assertEqual(data.get("user"), "carol")
92+
# helpers were called
93+
p_token.assert_called_once()
94+
p_info.assert_called_once()
95+
4096

4197
class SecureFileFieldTest(APITestCase):
4298
def is_valid_uuid(self, uuid_to_test):

api/views.py

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import json
2+
import os
23
import secrets
34
from datetime import datetime, timedelta
45
from urllib.parse import urlparse
56

7+
import requests
8+
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
69
from django.conf import settings
710
from django.contrib.auth import authenticate, login, logout
811
from django.contrib.auth.models import User
@@ -1068,32 +1071,109 @@ def logout_user(request):
10681071
return redirect(reverse(settings.LOGIN_URL))
10691072

10701073

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+
10711136
class AuthPowerBI(APIView):
10721137
authentication_classes = (authentication.SessionAuthentication,)
10731138
permission_classes = (permissions.IsAuthenticated,)
10741139

10751140
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
10771165
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
10781168
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}"
10871169

10881170
return Response(
10891171
{
10901172
"detail": "ok",
1091-
"embed_token": embed_token,
10921173
"embed_url": embed_url,
1093-
"expires_at": expires_at,
1094-
"group_id": group_id,
10951174
"report_id": report_id,
1096-
"report_section": report_section,
1175+
"embed_token": embed_token,
1176+
"expires_at": expires_at,
10971177
"user": request.user.username,
10981178
}
10991179
)

0 commit comments

Comments
 (0)