@luquimbo/bi-superpowers 1.0.0
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.
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +25 -0
- package/AGENTS.md +244 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/bin/build-plugin.js +30 -0
- package/bin/cli.js +1064 -0
- package/bin/commands/add.js +533 -0
- package/bin/commands/add.test.js +77 -0
- package/bin/commands/build-desktop.js +166 -0
- package/bin/commands/changelog.js +443 -0
- package/bin/commands/diff.js +325 -0
- package/bin/commands/lint.js +419 -0
- package/bin/commands/lint.test.js +103 -0
- package/bin/commands/mcp-setup.js +246 -0
- package/bin/commands/pull.js +287 -0
- package/bin/commands/pull.test.js +36 -0
- package/bin/commands/push.js +231 -0
- package/bin/commands/push.test.js +14 -0
- package/bin/commands/search.js +344 -0
- package/bin/commands/search.test.js +115 -0
- package/bin/commands/setup.js +545 -0
- package/bin/commands/setup.test.js +46 -0
- package/bin/commands/sync-profile.js +405 -0
- package/bin/commands/sync-profile.test.js +14 -0
- package/bin/commands/sync-source.js +418 -0
- package/bin/commands/sync-source.test.js +14 -0
- package/bin/commands/watch.js +206 -0
- package/bin/lib/generators/claude-plugin.js +266 -0
- package/bin/lib/generators/claude-plugin.test.js +110 -0
- package/bin/lib/generators/index.js +116 -0
- package/bin/lib/generators/shared.js +282 -0
- package/bin/lib/licensing/index.js +35 -0
- package/bin/lib/licensing/storage.js +364 -0
- package/bin/lib/licensing/storage.test.js +55 -0
- package/bin/lib/licensing/validator.js +213 -0
- package/bin/lib/licensing/validator.test.js +137 -0
- package/bin/lib/microsoft-mcp.js +176 -0
- package/bin/lib/microsoft-mcp.test.js +106 -0
- package/bin/lib/skills.js +84 -0
- package/bin/mcp/powerbi-modeling-launcher.js +38 -0
- package/bin/postinstall.js +44 -0
- package/bin/utils/errors.js +159 -0
- package/bin/utils/git.js +298 -0
- package/bin/utils/logger.js +142 -0
- package/bin/utils/mcp-detect.js +274 -0
- package/bin/utils/mcp-detect.test.js +105 -0
- package/bin/utils/pbix.js +305 -0
- package/bin/utils/pbix.test.js +37 -0
- package/bin/utils/profiles.js +312 -0
- package/bin/utils/projects.js +168 -0
- package/bin/utils/readline.js +206 -0
- package/bin/utils/readline.test.js +47 -0
- package/bin/utils/tui.js +314 -0
- package/bin/utils/tui.test.js +127 -0
- package/commands/contributions.md +265 -0
- package/commands/data-model-design.md +468 -0
- package/commands/dax-doctor.md +248 -0
- package/commands/fabric-scripts.md +452 -0
- package/commands/migration-assistant.md +290 -0
- package/commands/model-documenter.md +242 -0
- package/commands/pbi-connect.md +239 -0
- package/commands/project-kickoff.md +905 -0
- package/commands/report-layout.md +296 -0
- package/commands/rls-design.md +533 -0
- package/commands/theme-tweaker.md +624 -0
- package/config.example.json +23 -0
- package/config.json +23 -0
- package/desktop-extension/manifest.json +37 -0
- package/desktop-extension/package.json +10 -0
- package/desktop-extension/server.js +95 -0
- package/docs/openrouter-free-models.md +92 -0
- package/library/examples/README.md +151 -0
- package/library/examples/finance-reporting/README.md +351 -0
- package/library/examples/finance-reporting/data-model.md +267 -0
- package/library/examples/finance-reporting/measures.dax +557 -0
- package/library/examples/hr-analytics/README.md +371 -0
- package/library/examples/hr-analytics/data-model.md +315 -0
- package/library/examples/hr-analytics/measures.dax +460 -0
- package/library/examples/marketing-analytics/README.md +37 -0
- package/library/examples/marketing-analytics/data-model.md +62 -0
- package/library/examples/marketing-analytics/measures.dax +110 -0
- package/library/examples/retail-analytics/README.md +439 -0
- package/library/examples/retail-analytics/data-model.md +288 -0
- package/library/examples/retail-analytics/measures.dax +481 -0
- package/library/examples/supply-chain/README.md +37 -0
- package/library/examples/supply-chain/data-model.md +69 -0
- package/library/examples/supply-chain/measures.dax +77 -0
- package/library/examples/udf-library/README.md +228 -0
- package/library/examples/udf-library/functions.dax +571 -0
- package/library/snippets/dax/README.md +292 -0
- package/library/snippets/dax/business-domains.md +576 -0
- package/library/snippets/dax/calculate-patterns.md +276 -0
- package/library/snippets/dax/calculation-groups.md +489 -0
- package/library/snippets/dax/error-handling.md +495 -0
- package/library/snippets/dax/iterators-and-aggregations.md +474 -0
- package/library/snippets/dax/kpis-and-metrics.md +293 -0
- package/library/snippets/dax/rankings-and-topn.md +235 -0
- package/library/snippets/dax/security-patterns.md +413 -0
- package/library/snippets/dax/text-and-formatting.md +316 -0
- package/library/snippets/dax/time-intelligence.md +196 -0
- package/library/snippets/dax/user-defined-functions.md +477 -0
- package/library/snippets/dax/virtual-tables.md +546 -0
- package/library/snippets/excel-formulas/README.md +84 -0
- package/library/snippets/excel-formulas/aggregations.md +330 -0
- package/library/snippets/excel-formulas/dates-and-times.md +361 -0
- package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
- package/library/snippets/excel-formulas/lookups.md +169 -0
- package/library/snippets/excel-formulas/text-functions.md +363 -0
- package/library/snippets/governance/naming-conventions.md +97 -0
- package/library/snippets/governance/review-checklists.md +107 -0
- package/library/snippets/power-query/README.md +389 -0
- package/library/snippets/power-query/api-integration.md +707 -0
- package/library/snippets/power-query/connections.md +434 -0
- package/library/snippets/power-query/data-cleaning.md +298 -0
- package/library/snippets/power-query/error-handling.md +526 -0
- package/library/snippets/power-query/parameters.md +350 -0
- package/library/snippets/power-query/performance.md +506 -0
- package/library/snippets/power-query/transformations.md +330 -0
- package/library/snippets/report-design/accessibility.md +78 -0
- package/library/snippets/report-design/chart-selection.md +54 -0
- package/library/snippets/report-design/layout-patterns.md +87 -0
- package/library/templates/data-models/README.md +93 -0
- package/library/templates/data-models/finance-model.md +627 -0
- package/library/templates/data-models/retail-star-schema.md +473 -0
- package/library/templates/excel/README.md +83 -0
- package/library/templates/excel/budget-tracker.md +432 -0
- package/library/templates/excel/data-entry-form.md +533 -0
- package/library/templates/power-bi/README.md +72 -0
- package/library/templates/power-bi/finance-report.md +449 -0
- package/library/templates/power-bi/kpi-scorecard.md +461 -0
- package/library/templates/power-bi/sales-dashboard.md +281 -0
- package/library/themes/excel/README.md +436 -0
- package/library/themes/power-bi/README.md +271 -0
- package/library/themes/power-bi/accessible.json +307 -0
- package/library/themes/power-bi/bi-superpowers-default.json +858 -0
- package/library/themes/power-bi/corporate-blue.json +291 -0
- package/library/themes/power-bi/dark-mode.json +291 -0
- package/library/themes/power-bi/minimal.json +292 -0
- package/library/themes/power-bi/print-friendly.json +309 -0
- package/package.json +93 -0
- package/skills/contributions/SKILL.md +267 -0
- package/skills/data-model-design/SKILL.md +470 -0
- package/skills/data-modeling/SKILL.md +254 -0
- package/skills/data-quality/SKILL.md +664 -0
- package/skills/dax/SKILL.md +708 -0
- package/skills/dax-doctor/SKILL.md +250 -0
- package/skills/dax-udf/SKILL.md +489 -0
- package/skills/deployment/SKILL.md +320 -0
- package/skills/excel-formulas/SKILL.md +463 -0
- package/skills/fabric-scripts/SKILL.md +454 -0
- package/skills/fast-standard/SKILL.md +509 -0
- package/skills/governance/SKILL.md +205 -0
- package/skills/migration-assistant/SKILL.md +292 -0
- package/skills/model-documenter/SKILL.md +244 -0
- package/skills/pbi-connect/SKILL.md +241 -0
- package/skills/power-query/SKILL.md +406 -0
- package/skills/project-kickoff/SKILL.md +907 -0
- package/skills/query-performance/SKILL.md +480 -0
- package/skills/report-design/SKILL.md +207 -0
- package/skills/report-layout/SKILL.md +298 -0
- package/skills/rls-design/SKILL.md +535 -0
- package/skills/semantic-model/SKILL.md +237 -0
- package/skills/testing-validation/SKILL.md +643 -0
- package/skills/theme-tweaker/SKILL.md +626 -0
- package/src/content/base.md +237 -0
- package/src/content/mcp-requirements.json +69 -0
- package/src/content/routing.md +203 -0
- package/src/content/skills/contributions.md +259 -0
- package/src/content/skills/data-model-design.md +462 -0
- package/src/content/skills/data-modeling.md +246 -0
- package/src/content/skills/data-quality.md +656 -0
- package/src/content/skills/dax-doctor.md +242 -0
- package/src/content/skills/dax-udf.md +481 -0
- package/src/content/skills/dax.md +700 -0
- package/src/content/skills/deployment.md +312 -0
- package/src/content/skills/excel-formulas.md +455 -0
- package/src/content/skills/fabric-scripts.md +446 -0
- package/src/content/skills/fast-standard.md +501 -0
- package/src/content/skills/governance.md +197 -0
- package/src/content/skills/migration-assistant.md +284 -0
- package/src/content/skills/model-documenter.md +236 -0
- package/src/content/skills/pbi-connect.md +233 -0
- package/src/content/skills/power-query.md +398 -0
- package/src/content/skills/project-kickoff.md +899 -0
- package/src/content/skills/query-performance.md +472 -0
- package/src/content/skills/report-design.md +199 -0
- package/src/content/skills/report-layout.md +290 -0
- package/src/content/skills/rls-design.md +527 -0
- package/src/content/skills/semantic-model.md +229 -0
- package/src/content/skills/testing-validation.md +635 -0
- package/src/content/skills/theme-tweaker.md +618 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# Security Patterns in DAX
|
|
2
|
+
|
|
3
|
+
Row-Level Security (RLS), Object-Level Security (OLS), and dynamic security patterns.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Row-Level Security (RLS)
|
|
8
|
+
|
|
9
|
+
### Basic Static RLS
|
|
10
|
+
|
|
11
|
+
```dax
|
|
12
|
+
// Role: Sales Region - West
|
|
13
|
+
// Filter on Region dimension
|
|
14
|
+
[Region] = "West"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```dax
|
|
18
|
+
// Role: Single Country
|
|
19
|
+
// Filter on Geography
|
|
20
|
+
[Country] = "USA"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### User-Based Dynamic RLS
|
|
24
|
+
|
|
25
|
+
```dax
|
|
26
|
+
// Filter by logged-in user's email
|
|
27
|
+
// Requires DimUser table with Email and allowed regions
|
|
28
|
+
VAR _UserEmail = USERPRINCIPALNAME()
|
|
29
|
+
VAR _AllowedRegions =
|
|
30
|
+
CALCULATETABLE(
|
|
31
|
+
VALUES(DimUserRegion[Region]),
|
|
32
|
+
DimUserRegion[UserEmail] = _UserEmail
|
|
33
|
+
)
|
|
34
|
+
RETURN
|
|
35
|
+
[Region] IN _AllowedRegions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```dax
|
|
39
|
+
// Simplified: User sees their own data only
|
|
40
|
+
[EmployeeEmail] = USERPRINCIPALNAME()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Manager Hierarchy RLS
|
|
44
|
+
|
|
45
|
+
```dax
|
|
46
|
+
// Manager sees their direct reports and all indirect reports
|
|
47
|
+
// Requires parent-child hierarchy in employee table
|
|
48
|
+
VAR _UserEmail = USERPRINCIPALNAME()
|
|
49
|
+
VAR _ManagerEmployeeKey =
|
|
50
|
+
LOOKUPVALUE(
|
|
51
|
+
DimEmployee[EmployeeKey],
|
|
52
|
+
DimEmployee[Email], _UserEmail
|
|
53
|
+
)
|
|
54
|
+
VAR _DirectReports =
|
|
55
|
+
CALCULATETABLE(
|
|
56
|
+
VALUES(DimEmployee[EmployeeKey]),
|
|
57
|
+
PATH(DimEmployee[EmployeeKey], DimEmployee[ManagerKey]),
|
|
58
|
+
PATHCONTAINS(
|
|
59
|
+
PATH(DimEmployee[EmployeeKey], DimEmployee[ManagerKey]),
|
|
60
|
+
_ManagerEmployeeKey
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
RETURN
|
|
64
|
+
[EmployeeKey] IN _DirectReports ||
|
|
65
|
+
[EmployeeKey] = _ManagerEmployeeKey
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Multi-Table RLS
|
|
69
|
+
|
|
70
|
+
```dax
|
|
71
|
+
// Sales table filtered by user's region
|
|
72
|
+
// Must apply to the dimension table, not fact table
|
|
73
|
+
// In DimRegion table:
|
|
74
|
+
VAR _UserEmail = USERPRINCIPALNAME()
|
|
75
|
+
VAR _UserRegions =
|
|
76
|
+
CALCULATETABLE(
|
|
77
|
+
VALUES(DimUserAccess[RegionKey]),
|
|
78
|
+
DimUserAccess[Email] = _UserEmail
|
|
79
|
+
)
|
|
80
|
+
RETURN
|
|
81
|
+
[RegionKey] IN _UserRegions
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Security Tables Pattern
|
|
87
|
+
|
|
88
|
+
### User-Region Mapping Table
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
DimUserAccess
|
|
92
|
+
├── UserAccessKey (PK)
|
|
93
|
+
├── UserEmail
|
|
94
|
+
├── RegionKey (FK)
|
|
95
|
+
├── AccessLevel (Read/Write)
|
|
96
|
+
└── ValidFrom / ValidTo
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```dax
|
|
100
|
+
// RLS filter expression
|
|
101
|
+
VAR _User = USERPRINCIPALNAME()
|
|
102
|
+
RETURN
|
|
103
|
+
COUNTROWS(
|
|
104
|
+
FILTER(
|
|
105
|
+
DimUserAccess,
|
|
106
|
+
DimUserAccess[UserEmail] = _User &&
|
|
107
|
+
DimUserAccess[RegionKey] = [RegionKey] &&
|
|
108
|
+
DimUserAccess[ValidTo] >= TODAY()
|
|
109
|
+
)
|
|
110
|
+
) > 0
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Security with Effective Dating
|
|
114
|
+
|
|
115
|
+
```dax
|
|
116
|
+
// Time-based security (access expires)
|
|
117
|
+
VAR _User = USERPRINCIPALNAME()
|
|
118
|
+
VAR _Today = TODAY()
|
|
119
|
+
RETURN
|
|
120
|
+
COUNTROWS(
|
|
121
|
+
FILTER(
|
|
122
|
+
DimUserAccess,
|
|
123
|
+
DimUserAccess[UserEmail] = _User &&
|
|
124
|
+
DimUserAccess[RegionKey] = [RegionKey] &&
|
|
125
|
+
DimUserAccess[ValidFrom] <= _Today &&
|
|
126
|
+
(_Today <= DimUserAccess[ValidTo] || ISBLANK(DimUserAccess[ValidTo]))
|
|
127
|
+
)
|
|
128
|
+
) > 0
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Performance-Optimized RLS
|
|
134
|
+
|
|
135
|
+
### Pre-filtered Security Table
|
|
136
|
+
|
|
137
|
+
```dax
|
|
138
|
+
// Instead of complex filter in RLS, use pre-computed table
|
|
139
|
+
// DimUserFilteredRegions: One row per user-region combination
|
|
140
|
+
VAR _User = USERPRINCIPALNAME()
|
|
141
|
+
RETURN
|
|
142
|
+
[RegionKey] IN
|
|
143
|
+
CALCULATETABLE(
|
|
144
|
+
VALUES(DimUserFilteredRegions[RegionKey]),
|
|
145
|
+
DimUserFilteredRegions[UserEmail] = _User
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Avoid LOOKUPVALUE in RLS
|
|
150
|
+
|
|
151
|
+
```dax
|
|
152
|
+
// BAD: LOOKUPVALUE in every row evaluation
|
|
153
|
+
[RegionKey] = LOOKUPVALUE(DimUser[RegionKey], DimUser[Email], USERPRINCIPALNAME())
|
|
154
|
+
|
|
155
|
+
// GOOD: Use IN with VALUES
|
|
156
|
+
VAR _User = USERPRINCIPALNAME()
|
|
157
|
+
RETURN
|
|
158
|
+
[RegionKey] IN CALCULATETABLE(
|
|
159
|
+
VALUES(DimUser[RegionKey]),
|
|
160
|
+
DimUser[Email] = _User
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Bi-directional Filtering Consideration
|
|
165
|
+
|
|
166
|
+
```dax
|
|
167
|
+
// If relationship is bi-directional, RLS on one table filters both
|
|
168
|
+
// Consider setting relationship to single direction for security tables
|
|
169
|
+
// Then explicitly cross-filter in measures when needed
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Testing RLS in DAX
|
|
175
|
+
|
|
176
|
+
### Test as Another User
|
|
177
|
+
|
|
178
|
+
```dax
|
|
179
|
+
// Measure to simulate RLS for testing
|
|
180
|
+
Sales_AsUser =
|
|
181
|
+
VAR _TestUser = "john.smith@company.com" // Replace with test user
|
|
182
|
+
VAR _AllowedRegions =
|
|
183
|
+
CALCULATETABLE(
|
|
184
|
+
VALUES(DimUserAccess[RegionKey]),
|
|
185
|
+
DimUserAccess[UserEmail] = _TestUser
|
|
186
|
+
)
|
|
187
|
+
RETURN
|
|
188
|
+
CALCULATE(
|
|
189
|
+
[Sales],
|
|
190
|
+
DimRegion[RegionKey] IN _AllowedRegions
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### RLS Debug Measure
|
|
195
|
+
|
|
196
|
+
```dax
|
|
197
|
+
// Shows what user sees for debugging
|
|
198
|
+
_RLS_Debug =
|
|
199
|
+
VAR _User = USERPRINCIPALNAME()
|
|
200
|
+
VAR _AccessCount =
|
|
201
|
+
COUNTROWS(
|
|
202
|
+
FILTER(
|
|
203
|
+
DimUserAccess,
|
|
204
|
+
DimUserAccess[UserEmail] = _User
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
RETURN
|
|
208
|
+
"User: " & _User &
|
|
209
|
+
" | Access Rows: " & _AccessCount &
|
|
210
|
+
" | Regions: " & CONCATENATEX(
|
|
211
|
+
FILTER(DimUserAccess, DimUserAccess[UserEmail] = _User),
|
|
212
|
+
DimUserAccess[RegionName],
|
|
213
|
+
", "
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Object-Level Security (OLS)
|
|
220
|
+
|
|
221
|
+
### Restricting Column Access
|
|
222
|
+
|
|
223
|
+
OLS is configured in the model (not DAX), but here's how to handle hidden data in measures:
|
|
224
|
+
|
|
225
|
+
```dax
|
|
226
|
+
// Measure that respects OLS
|
|
227
|
+
// If Salary column is restricted, this returns blank for unauthorized users
|
|
228
|
+
Salary Total =
|
|
229
|
+
VAR _Result = SUM(FactEmployees[Salary])
|
|
230
|
+
RETURN
|
|
231
|
+
IF(ISNUMBER(_Result), _Result, BLANK())
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Conditional Column Display
|
|
235
|
+
|
|
236
|
+
```dax
|
|
237
|
+
// Show value only if user has access (via security table)
|
|
238
|
+
Salary Display =
|
|
239
|
+
VAR _User = USERPRINCIPALNAME()
|
|
240
|
+
VAR _HasAccess =
|
|
241
|
+
CALCULATE(
|
|
242
|
+
COUNTROWS(DimUserPermissions),
|
|
243
|
+
DimUserPermissions[UserEmail] = _User,
|
|
244
|
+
DimUserPermissions[Permission] = "ViewSalary"
|
|
245
|
+
) > 0
|
|
246
|
+
RETURN
|
|
247
|
+
IF(_HasAccess, [Salary Total], BLANK())
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Dynamic Security Patterns
|
|
253
|
+
|
|
254
|
+
### Role-Based Access
|
|
255
|
+
|
|
256
|
+
```dax
|
|
257
|
+
// Different data based on user's role
|
|
258
|
+
Sales_RoleBased =
|
|
259
|
+
VAR _User = USERPRINCIPALNAME()
|
|
260
|
+
VAR _Role = LOOKUPVALUE(DimUser[Role], DimUser[Email], _User)
|
|
261
|
+
RETURN
|
|
262
|
+
SWITCH(
|
|
263
|
+
_Role,
|
|
264
|
+
"Admin", [Sales], // See all
|
|
265
|
+
"Manager", CALCULATE([Sales], DimRegion[Region] = LOOKUPVALUE(DimUser[Region], DimUser[Email], _User)),
|
|
266
|
+
"Sales Rep", CALCULATE([Sales], DimSalesRep[Email] = _User),
|
|
267
|
+
BLANK() // No access
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Attribute-Based Access Control (ABAC)
|
|
272
|
+
|
|
273
|
+
```dax
|
|
274
|
+
// Access based on multiple attributes
|
|
275
|
+
VAR _User = USERPRINCIPALNAME()
|
|
276
|
+
VAR _UserDept = LOOKUPVALUE(DimUser[Department], DimUser[Email], _User)
|
|
277
|
+
VAR _UserLevel = LOOKUPVALUE(DimUser[Level], DimUser[Email], _User)
|
|
278
|
+
VAR _UserRegion = LOOKUPVALUE(DimUser[Region], DimUser[Email], _User)
|
|
279
|
+
RETURN
|
|
280
|
+
-- Level 5+ sees all
|
|
281
|
+
_UserLevel >= 5 ||
|
|
282
|
+
-- Same department and region
|
|
283
|
+
([Department] = _UserDept && [Region] = _UserRegion) ||
|
|
284
|
+
-- Specific cross-department access
|
|
285
|
+
[Department] IN {"Shared Services", "Corporate"}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Common Security Scenarios
|
|
291
|
+
|
|
292
|
+
### Sales Rep Sees Own Customers
|
|
293
|
+
|
|
294
|
+
```dax
|
|
295
|
+
// On DimCustomer table
|
|
296
|
+
VAR _User = USERPRINCIPALNAME()
|
|
297
|
+
VAR _SalesRepKey =
|
|
298
|
+
LOOKUPVALUE(DimSalesRep[SalesRepKey], DimSalesRep[Email], _User)
|
|
299
|
+
RETURN
|
|
300
|
+
[AssignedSalesRepKey] = _SalesRepKey ||
|
|
301
|
+
ISBLANK(_SalesRepKey) // Allow if user not found (for admins)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Regional Manager Sees Region + Corporate
|
|
305
|
+
|
|
306
|
+
```dax
|
|
307
|
+
// On DimRegion table
|
|
308
|
+
VAR _User = USERPRINCIPALNAME()
|
|
309
|
+
VAR _UserRegion =
|
|
310
|
+
LOOKUPVALUE(DimUser[Region], DimUser[Email], _User)
|
|
311
|
+
RETURN
|
|
312
|
+
[Region] = _UserRegion ||
|
|
313
|
+
[Region] = "Corporate" ||
|
|
314
|
+
ISBLANK(_UserRegion)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Time-Limited Project Access
|
|
318
|
+
|
|
319
|
+
```dax
|
|
320
|
+
// On DimProject table
|
|
321
|
+
VAR _User = USERPRINCIPALNAME()
|
|
322
|
+
VAR _Today = TODAY()
|
|
323
|
+
RETURN
|
|
324
|
+
COUNTROWS(
|
|
325
|
+
FILTER(
|
|
326
|
+
DimProjectAccess,
|
|
327
|
+
DimProjectAccess[ProjectKey] = [ProjectKey] &&
|
|
328
|
+
DimProjectAccess[UserEmail] = _User &&
|
|
329
|
+
DimProjectAccess[StartDate] <= _Today &&
|
|
330
|
+
DimProjectAccess[EndDate] >= _Today
|
|
331
|
+
)
|
|
332
|
+
) > 0
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Security Best Practices
|
|
338
|
+
|
|
339
|
+
### 1. Always Filter on Dimensions
|
|
340
|
+
|
|
341
|
+
```dax
|
|
342
|
+
// GOOD: RLS on dimension table
|
|
343
|
+
// Fact table inherits filter through relationship
|
|
344
|
+
// On DimRegion:
|
|
345
|
+
[Region] IN {"North", "South"}
|
|
346
|
+
|
|
347
|
+
// BAD: RLS on fact table (slow)
|
|
348
|
+
// On FactSales:
|
|
349
|
+
RELATED(DimRegion[Region]) IN {"North", "South"}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 2. Test with Different Users
|
|
353
|
+
|
|
354
|
+
```dax
|
|
355
|
+
// Create test measure before deploying
|
|
356
|
+
_Test_Security =
|
|
357
|
+
VAR _TestUser = "test.user@company.com"
|
|
358
|
+
RETURN
|
|
359
|
+
CALCULATE(
|
|
360
|
+
[Sales],
|
|
361
|
+
-- Simulate the RLS filter
|
|
362
|
+
DimRegion[Region] IN CALCULATETABLE(
|
|
363
|
+
VALUES(DimUserAccess[Region]),
|
|
364
|
+
DimUserAccess[UserEmail] = _TestUser
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 3. Provide Fallback for Admins
|
|
370
|
+
|
|
371
|
+
```dax
|
|
372
|
+
// Always allow admin role
|
|
373
|
+
VAR _User = USERPRINCIPALNAME()
|
|
374
|
+
VAR _IsAdmin = _User IN {"admin@company.com", "power.bi.admin@company.com"}
|
|
375
|
+
VAR _UserRegions = CALCULATETABLE(...)
|
|
376
|
+
RETURN
|
|
377
|
+
_IsAdmin || [RegionKey] IN _UserRegions
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### 4. Log Security Filters
|
|
381
|
+
|
|
382
|
+
```dax
|
|
383
|
+
// Measure to audit what users see
|
|
384
|
+
_Security_Audit =
|
|
385
|
+
"User: " & USERPRINCIPALNAME() &
|
|
386
|
+
" | Rows Visible: " & COUNTROWS(ALLSELECTED(FactSales)) &
|
|
387
|
+
" | Timestamp: " & NOW()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Troubleshooting
|
|
393
|
+
|
|
394
|
+
| Issue | Cause | Fix |
|
|
395
|
+
|-------|-------|-----|
|
|
396
|
+
| All data visible | RLS not applied | Check role membership |
|
|
397
|
+
| No data visible | Filter too restrictive | Verify user in security table |
|
|
398
|
+
| Slow with RLS | Complex RLS expression | Simplify, use pre-computed table |
|
|
399
|
+
| Inconsistent results | Bi-directional filters | Review relationship directions |
|
|
400
|
+
| Error with USERPRINCIPALNAME | Running in Desktop | Use "View as Role" feature |
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Setup Checklist
|
|
405
|
+
|
|
406
|
+
- [ ] Create security mapping table (user → access)
|
|
407
|
+
- [ ] Define RLS role in model
|
|
408
|
+
- [ ] Write filter expression on dimension table(s)
|
|
409
|
+
- [ ] Test with "View as Role" in Desktop
|
|
410
|
+
- [ ] Test with actual users in Service
|
|
411
|
+
- [ ] Verify totals match expected (no data leakage)
|
|
412
|
+
- [ ] Document security requirements
|
|
413
|
+
- [ ] Set up monitoring for access patterns
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Text and Formatting Patterns
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
DAX patterns for text manipulation, concatenation, and dynamic formatting in Power BI measures.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Text Concatenation
|
|
9
|
+
|
|
10
|
+
### Basic Concatenation
|
|
11
|
+
```dax
|
|
12
|
+
-- Using & operator
|
|
13
|
+
FullName = [FirstName] & " " & [LastName]
|
|
14
|
+
|
|
15
|
+
-- Using CONCATENATE (only 2 arguments)
|
|
16
|
+
FullName = CONCATENATE([FirstName], CONCATENATE(" ", [LastName]))
|
|
17
|
+
|
|
18
|
+
-- Using COMBINEVALUES (better for relationships)
|
|
19
|
+
CustomerKey = COMBINEVALUES("-", [Region], [CustomerID])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Dynamic Titles
|
|
23
|
+
```dax
|
|
24
|
+
ChartTitle =
|
|
25
|
+
VAR _SelectedYear = SELECTEDVALUE('Date'[Year], "All Years")
|
|
26
|
+
VAR _SelectedRegion = SELECTEDVALUE(Region[Name], "All Regions")
|
|
27
|
+
RETURN
|
|
28
|
+
"Sales for " & _SelectedRegion & " - " & _SelectedYear
|
|
29
|
+
|
|
30
|
+
-- With metric selection
|
|
31
|
+
DynamicTitle =
|
|
32
|
+
VAR _Metric = SELECTEDVALUE(MetricSelector[Metric], "Sales")
|
|
33
|
+
RETURN
|
|
34
|
+
_Metric & " by Region"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Conditional Text
|
|
38
|
+
```dax
|
|
39
|
+
StatusText =
|
|
40
|
+
VAR _Variance = [Actual] - [Budget]
|
|
41
|
+
RETURN
|
|
42
|
+
IF(_Variance >= 0, "On Target", "Below Target")
|
|
43
|
+
|
|
44
|
+
-- With emoji/symbols
|
|
45
|
+
TrendIndicator =
|
|
46
|
+
VAR _Change = [MoM Growth %]
|
|
47
|
+
RETURN
|
|
48
|
+
SWITCH(
|
|
49
|
+
TRUE(),
|
|
50
|
+
_Change > 0.05, "▲ Strong",
|
|
51
|
+
_Change > 0, "▲ Up",
|
|
52
|
+
_Change = 0, "► Flat",
|
|
53
|
+
_Change > -0.05, "▼ Down",
|
|
54
|
+
"▼ Weak"
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Number Formatting
|
|
61
|
+
|
|
62
|
+
### FORMAT Function
|
|
63
|
+
```dax
|
|
64
|
+
-- Currency
|
|
65
|
+
FormattedSales = FORMAT([Total Sales], "$#,##0.00")
|
|
66
|
+
|
|
67
|
+
-- Percentage
|
|
68
|
+
FormattedGrowth = FORMAT([Growth Rate], "0.0%")
|
|
69
|
+
|
|
70
|
+
-- Thousands/Millions
|
|
71
|
+
FormattedShort =
|
|
72
|
+
VAR _Value = [Total Sales]
|
|
73
|
+
RETURN
|
|
74
|
+
SWITCH(
|
|
75
|
+
TRUE(),
|
|
76
|
+
_Value >= 1000000000, FORMAT(_Value / 1000000000, "#,##0.0") & "B",
|
|
77
|
+
_Value >= 1000000, FORMAT(_Value / 1000000, "#,##0.0") & "M",
|
|
78
|
+
_Value >= 1000, FORMAT(_Value / 1000, "#,##0.0") & "K",
|
|
79
|
+
FORMAT(_Value, "#,##0")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
-- Custom decimal places
|
|
83
|
+
FormattedValue = FORMAT([Value], "#,##0.00")
|
|
84
|
+
|
|
85
|
+
-- Leading zeros
|
|
86
|
+
FormattedID = FORMAT([ID], "00000") -- 00123
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Date Formatting
|
|
90
|
+
```dax
|
|
91
|
+
-- Full date
|
|
92
|
+
FormattedDate = FORMAT([Date], "MMMM D, YYYY") -- January 15, 2024
|
|
93
|
+
|
|
94
|
+
-- Short date
|
|
95
|
+
ShortDate = FORMAT([Date], "MM/DD/YYYY")
|
|
96
|
+
|
|
97
|
+
-- Month-Year
|
|
98
|
+
MonthYear = FORMAT([Date], "MMM YYYY") -- Jan 2024
|
|
99
|
+
|
|
100
|
+
-- Day name
|
|
101
|
+
DayName = FORMAT([Date], "DDDD") -- Monday
|
|
102
|
+
|
|
103
|
+
-- ISO format
|
|
104
|
+
ISODate = FORMAT([Date], "YYYY-MM-DD")
|
|
105
|
+
|
|
106
|
+
-- Quarter
|
|
107
|
+
QuarterText = "Q" & FORMAT([Date], "Q YYYY") -- Q1 2024
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Conditional Formatting Values
|
|
111
|
+
```dax
|
|
112
|
+
-- For use with conditional formatting
|
|
113
|
+
VarianceColor =
|
|
114
|
+
VAR _Variance = [Actual] - [Budget]
|
|
115
|
+
RETURN
|
|
116
|
+
IF(_Variance >= 0, 1, -1)
|
|
117
|
+
|
|
118
|
+
-- Traffic light (1, 2, 3)
|
|
119
|
+
PerformanceLevel =
|
|
120
|
+
VAR _Achievement = DIVIDE([Actual], [Target])
|
|
121
|
+
RETURN
|
|
122
|
+
SWITCH(
|
|
123
|
+
TRUE(),
|
|
124
|
+
_Achievement >= 1, 1, -- Green
|
|
125
|
+
_Achievement >= 0.8, 2, -- Yellow
|
|
126
|
+
3 -- Red
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Dynamic Labels
|
|
133
|
+
|
|
134
|
+
### KPI Card Labels
|
|
135
|
+
```dax
|
|
136
|
+
SalesWithLabel =
|
|
137
|
+
VAR _Sales = [Total Sales]
|
|
138
|
+
VAR _FormattedSales = FORMAT(_Sales, "$#,##0")
|
|
139
|
+
RETURN
|
|
140
|
+
"Total Sales: " & _FormattedSales
|
|
141
|
+
|
|
142
|
+
-- With comparison
|
|
143
|
+
SalesComparison =
|
|
144
|
+
VAR _Current = [Total Sales]
|
|
145
|
+
VAR _Previous = [Total Sales PY]
|
|
146
|
+
VAR _Change = _Current - _Previous
|
|
147
|
+
VAR _Pct = DIVIDE(_Change, _Previous)
|
|
148
|
+
RETURN
|
|
149
|
+
FORMAT(_Current, "$#,##0") &
|
|
150
|
+
" (" & IF(_Change >= 0, "+", "") & FORMAT(_Pct, "0.0%") & " vs PY)"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Selection Labels
|
|
154
|
+
```dax
|
|
155
|
+
SelectionSummary =
|
|
156
|
+
VAR _SelectedProducts = COUNTROWS(VALUES(Product[Name]))
|
|
157
|
+
VAR _TotalProducts = COUNTROWS(ALL(Product[Name]))
|
|
158
|
+
RETURN
|
|
159
|
+
IF(
|
|
160
|
+
_SelectedProducts = _TotalProducts,
|
|
161
|
+
"All Products",
|
|
162
|
+
_SelectedProducts & " of " & _TotalProducts & " Products"
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Period Labels
|
|
167
|
+
```dax
|
|
168
|
+
PeriodLabel =
|
|
169
|
+
VAR _MinDate = MIN('Date'[Date])
|
|
170
|
+
VAR _MaxDate = MAX('Date'[Date])
|
|
171
|
+
RETURN
|
|
172
|
+
FORMAT(_MinDate, "MMM D, YYYY") & " - " & FORMAT(_MaxDate, "MMM D, YYYY")
|
|
173
|
+
|
|
174
|
+
-- Relative period
|
|
175
|
+
RelativePeriod =
|
|
176
|
+
VAR _Days = DATEDIFF(MAX('Date'[Date]), TODAY(), DAY)
|
|
177
|
+
RETURN
|
|
178
|
+
SWITCH(
|
|
179
|
+
TRUE(),
|
|
180
|
+
_Days = 0, "Today",
|
|
181
|
+
_Days = 1, "Yesterday",
|
|
182
|
+
_Days <= 7, _Days & " days ago",
|
|
183
|
+
_Days <= 30, ROUNDUP(_Days / 7, 0) & " weeks ago",
|
|
184
|
+
FORMAT(MAX('Date'[Date]), "MMM D, YYYY")
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Text Extraction and Manipulation
|
|
191
|
+
|
|
192
|
+
### SEARCH and MID
|
|
193
|
+
```dax
|
|
194
|
+
-- Extract domain from email
|
|
195
|
+
EmailDomain =
|
|
196
|
+
VAR _Email = [Email]
|
|
197
|
+
VAR _AtPos = SEARCH("@", _Email, 1, 0)
|
|
198
|
+
RETURN
|
|
199
|
+
IF(_AtPos > 0, MID(_Email, _AtPos + 1, 100), BLANK())
|
|
200
|
+
|
|
201
|
+
-- Extract first word
|
|
202
|
+
FirstWord =
|
|
203
|
+
VAR _Text = [FullName]
|
|
204
|
+
VAR _SpacePos = SEARCH(" ", _Text, 1, LEN(_Text) + 1)
|
|
205
|
+
RETURN
|
|
206
|
+
LEFT(_Text, _SpacePos - 1)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Text Cleaning
|
|
210
|
+
```dax
|
|
211
|
+
-- Remove extra spaces (calculated column)
|
|
212
|
+
CleanedName = TRIM([Name])
|
|
213
|
+
|
|
214
|
+
-- Proper case (calculated column)
|
|
215
|
+
ProperName =
|
|
216
|
+
VAR _Text = LOWER([Name])
|
|
217
|
+
RETURN
|
|
218
|
+
UPPER(LEFT(_Text, 1)) & MID(_Text, 2, LEN(_Text))
|
|
219
|
+
|
|
220
|
+
-- Replace characters (calculated column)
|
|
221
|
+
CleanPhone = SUBSTITUTE(SUBSTITUTE([Phone], "-", ""), " ", "")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### UNICHAR for Symbols
|
|
225
|
+
```dax
|
|
226
|
+
-- Common symbols
|
|
227
|
+
CheckMark = UNICHAR(10004) -- ✔
|
|
228
|
+
CrossMark = UNICHAR(10006) -- ✖
|
|
229
|
+
UpArrow = UNICHAR(9650) -- ▲
|
|
230
|
+
DownArrow = UNICHAR(9660) -- ▼
|
|
231
|
+
RightArrow = UNICHAR(9658) -- ►
|
|
232
|
+
Star = UNICHAR(9733) -- ★
|
|
233
|
+
Circle = UNICHAR(9679) -- ●
|
|
234
|
+
|
|
235
|
+
-- Usage in measure
|
|
236
|
+
StatusIcon =
|
|
237
|
+
VAR _Status = [Status]
|
|
238
|
+
RETURN
|
|
239
|
+
SWITCH(
|
|
240
|
+
_Status,
|
|
241
|
+
"Complete", UNICHAR(10004),
|
|
242
|
+
"In Progress", UNICHAR(9679),
|
|
243
|
+
"Not Started", UNICHAR(10006),
|
|
244
|
+
""
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Ranking and Position Text
|
|
251
|
+
|
|
252
|
+
### Ordinal Numbers
|
|
253
|
+
```dax
|
|
254
|
+
OrdinalRank =
|
|
255
|
+
VAR _Rank = [Rank]
|
|
256
|
+
VAR _Suffix =
|
|
257
|
+
SWITCH(
|
|
258
|
+
TRUE(),
|
|
259
|
+
MOD(_Rank, 100) IN {11, 12, 13}, "th",
|
|
260
|
+
MOD(_Rank, 10) = 1, "st",
|
|
261
|
+
MOD(_Rank, 10) = 2, "nd",
|
|
262
|
+
MOD(_Rank, 10) = 3, "rd",
|
|
263
|
+
"th"
|
|
264
|
+
)
|
|
265
|
+
RETURN
|
|
266
|
+
_Rank & _Suffix
|
|
267
|
+
|
|
268
|
+
-- Result: 1st, 2nd, 3rd, 4th, 11th, 12th, 21st, etc.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Ranking Label
|
|
272
|
+
```dax
|
|
273
|
+
RankLabel =
|
|
274
|
+
VAR _Rank = [ProductRank]
|
|
275
|
+
VAR _Total = COUNTROWS(ALL(Product))
|
|
276
|
+
RETURN
|
|
277
|
+
"#" & _Rank & " of " & _Total
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Multi-line Text
|
|
283
|
+
|
|
284
|
+
### Line Breaks
|
|
285
|
+
```dax
|
|
286
|
+
-- Using UNICHAR(10) for line break
|
|
287
|
+
MultiLineKPI =
|
|
288
|
+
VAR _Sales = FORMAT([Total Sales], "$#,##0")
|
|
289
|
+
VAR _Units = FORMAT([Total Units], "#,##0")
|
|
290
|
+
VAR _Avg = FORMAT([Avg Order Value], "$#,##0.00")
|
|
291
|
+
RETURN
|
|
292
|
+
"Sales: " & _Sales & UNICHAR(10) &
|
|
293
|
+
"Units: " & _Units & UNICHAR(10) &
|
|
294
|
+
"AOV: " & _Avg
|
|
295
|
+
|
|
296
|
+
-- Address formatting
|
|
297
|
+
FullAddress =
|
|
298
|
+
[Street] & UNICHAR(10) &
|
|
299
|
+
[City] & ", " & [State] & " " & [PostalCode]
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Performance Note
|
|
305
|
+
|
|
306
|
+
- FORMAT() returns text - can't be used in further calculations
|
|
307
|
+
- Use separate measures: one for value (number), one for display (text)
|
|
308
|
+
- Text measures increase model size - use sparingly
|
|
309
|
+
- Consider using Power BI's built-in formatting where possible
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Related Resources
|
|
314
|
+
|
|
315
|
+
- [KPIs and Metrics](./kpis-and-metrics.md)
|
|
316
|
+
- [DAX Skill](../../skills/dax/SKILL.md)
|