dara-core 1.23.0a1__py3-none-any.whl → 1.23.2a1__py3-none-any.whl

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.
@@ -27789,8 +27789,10 @@
27789
27789
  `;
27790
27790
  styled.div`
27791
27791
  display: flex;
27792
+ align-items: center;
27792
27793
  gap: 0.5rem;
27793
27794
  width: 100%;
27795
+ line-height: 1;
27794
27796
  `;
27795
27797
  styled.div`
27796
27798
  color: ${(props) => props.theme.colors.grey3};
@@ -39077,6 +39079,8 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39077
39079
  style: Object.assign(Object.assign(Object.assign({}, floatingStyles), { maxHeight: 800, minWidth: 150, zIndex: 9999 }), style)
39078
39080
  }), { isOpen, children: jsxRuntimeExports.jsx(SectionedList, { items: allowColumnHiding ? [resetFunctions, columnToggles] : [resetFunctions], onSelect: onOptionSelect }, isOpen ? "open" : "closed") })), document.body)] });
39079
39081
  };
39082
+ const { fontSize } = window.getComputedStyle(document.documentElement);
39083
+ const DEFAULT_ROW_HEIGHT = parseFloat(fontSize) * 2.5;
39080
39084
  var __rest$1 = function(s, e2) {
39081
39085
  var t2 = {};
39082
39086
  for (var p2 in s) if (Object.prototype.hasOwnProperty.call(s, p2) && e2.indexOf(p2) < 0)
@@ -39088,8 +39092,6 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39088
39092
  }
39089
39093
  return t2;
39090
39094
  };
39091
- const { fontSize } = window.getComputedStyle(document.documentElement);
39092
- const ROW_HEIGHT = parseFloat(fontSize) * 2.5;
39093
39095
  const shouldForwardProp$2 = (prop) => !["isSorted", "onClickRow"].includes(prop);
39094
39096
  const Row = styled.div.withConfig({ shouldForwardProp: shouldForwardProp$2 })`
39095
39097
  cursor: ${(props) => props.onClickRow ? "pointer" : "default"};
@@ -39145,7 +39147,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39145
39147
  align-items: center;
39146
39148
 
39147
39149
  min-width: 80px;
39148
- height: ${() => `${ROW_HEIGHT}px`};
39150
+ height: ${({ rowHeight }) => `${rowHeight}px`};
39149
39151
 
39150
39152
  color: ${(props) => props.theme.colors.grey6};
39151
39153
 
@@ -39169,7 +39171,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39169
39171
  var _a;
39170
39172
  return areEqual(prevProps, nextProps) && !(((_a = nextProps.data) === null || _a === void 0 ? void 0 : _a.headerGroups) || []).some((headerGroup) => ((headerGroup === null || headerGroup === void 0 ? void 0 : headerGroup.headers) || []).some((header) => header.isResizing));
39171
39173
  };
39172
- const RenderRow = React__namespace.memo(({ data: { width, currentEditCell, headerGroups, rows, prepareRow, getItem, totalColumnsWidth, onClickRow, throttledClickRow, backgroundColor, mappedColumns }, index: index2, style: renderRowStyle }) => {
39174
+ const RenderRow = React__namespace.memo(({ data: { width, currentEditCell, headerGroups, rows, prepareRow, getItem, totalColumnsWidth, onClickRow, throttledClickRow, backgroundColor, mappedColumns, rowHeight }, index: index2, style: renderRowStyle }) => {
39173
39175
  let row = rows[index2];
39174
39176
  if (getItem) {
39175
39177
  const value = getItem(index2);
@@ -39182,8 +39184,8 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39182
39184
  }
39183
39185
  if (!row) {
39184
39186
  return jsxRuntimeExports.jsx("div", { children: headerGroups.map((headerGroup, gidx) => jsxRuntimeExports.jsx(RowPlaceholder, { style: {
39185
- height: ROW_HEIGHT,
39186
- top: (index2 + 1) * ROW_HEIGHT,
39187
+ height: rowHeight,
39188
+ top: (index2 + 1) * rowHeight,
39187
39189
  width: totalColumnsWidth > width ? totalColumnsWidth : "100%"
39188
39190
  }, children: headerGroup === null || headerGroup === void 0 ? void 0 : headerGroup.headers.map((col, cidx) => {
39189
39191
  const headerProps = col.getHeaderProps();
@@ -39201,15 +39203,23 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39201
39203
  }
39202
39204
  };
39203
39205
  const _a = row.getRowProps({ style: renderRowStyle }), { style: rowStyle } = _a, restRow = __rest$1(_a, ["style"]);
39204
- return React$1.createElement(Row, Object.assign({}, restRow, { key: `row-${index2}`, onClick, onClickRow, style: Object.assign(Object.assign({}, rowStyle), { top: (index2 + 1) * ROW_HEIGHT, width: totalColumnsWidth > width ? totalColumnsWidth : "100%" }) }), row.cells.map((cell, colIdx) => {
39205
- var _a2;
39206
+ return React$1.createElement(Row, Object.assign({}, restRow, { key: `row-${index2}`, onClick, onClickRow, style: Object.assign(Object.assign({}, rowStyle), {
39207
+ // The first row is the header row which is not controlled by this rowHeight prop so it needs to be part of the calculation.
39208
+ top: index2 === 0 ? DEFAULT_ROW_HEIGHT : index2 * rowHeight + DEFAULT_ROW_HEIGHT,
39209
+ width: totalColumnsWidth > width ? totalColumnsWidth : "100%"
39210
+ }) }), row.cells.map((cell, colIdx) => {
39211
+ var _a2, _b, _c, _d, _e2;
39206
39212
  const cellProps = cell.getCellProps();
39207
39213
  return React$1.createElement(
39208
39214
  Cell,
39209
- Object.assign({}, cellProps, { key: `cell-${index2}-${colIdx}`, style: Object.assign(Object.assign({}, cellProps.style), { backgroundColor, justifyContent: mappedColumns[colIdx].align, maxWidth: (_a2 = cell.column) === null || _a2 === void 0 ? void 0 : _a2.maxWidth, width: (
39215
+ Object.assign({}, cellProps, { rowHeight, key: `cell-${index2}-${colIdx}`, style: Object.assign(Object.assign(Object.assign(Object.assign({}, cellProps.style), { backgroundColor, justifyContent: mappedColumns[colIdx].align, maxWidth: (_a2 = cell.column) === null || _a2 === void 0 ? void 0 : _a2.maxWidth, width: (
39210
39216
  // If width calc has messed up then use the raw width from the column
39211
39217
  cellProps.style.width === "NaNpx" ? mappedColumns[colIdx].width : cellProps.style.width
39212
- ) }) }),
39218
+ ) }), ((_b = mappedColumns[colIdx]) === null || _b === void 0 ? void 0 : _b.sticky) === "left" && typeof ((_c = mappedColumns[colIdx]) === null || _c === void 0 ? void 0 : _c.stickyOffset) === "number" ? {
39219
+ left: `${mappedColumns[colIdx].stickyOffset}px`
39220
+ } : {}), ((_d = mappedColumns[colIdx]) === null || _d === void 0 ? void 0 : _d.sticky) === "right" && typeof ((_e2 = mappedColumns[colIdx]) === null || _e2 === void 0 ? void 0 : _e2.stickyOffset) === "number" ? {
39221
+ right: `${mappedColumns[colIdx].stickyOffset}px`
39222
+ } : {}) }),
39213
39223
  jsxRuntimeExports.jsx(CellContent, { children: cell.render("Cell", {
39214
39224
  colIdx,
39215
39225
  currentEditCell,
@@ -39290,7 +39300,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39290
39300
  justify-content: space-between;
39291
39301
 
39292
39302
  min-width: 80px;
39293
- height: ${ROW_HEIGHT}px;
39303
+ height: ${DEFAULT_ROW_HEIGHT}px;
39294
39304
 
39295
39305
  color: ${(props) => props.theme.colors.text};
39296
39306
 
@@ -39375,6 +39385,26 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39375
39385
  });
39376
39386
  return [...leftStickyCols, ...nonStickyCols, ...rightStickyCols];
39377
39387
  };
39388
+ const appendStickyOffsets = (columns) => {
39389
+ let leftOffset = 0;
39390
+ let rightOffset = 0;
39391
+ const rightStickyColumnWidths = columns.filter((col) => col.sticky === "right").slice(1).map((col) => parseInt(col.width) || 150);
39392
+ return columns.map((col) => {
39393
+ if (col.sticky === "left") {
39394
+ const nextCol = Object.assign(Object.assign({}, col), { stickyOffset: leftOffset });
39395
+ const width = parseInt(col.width) || 150;
39396
+ leftOffset += width;
39397
+ return nextCol;
39398
+ }
39399
+ if (col.sticky === "right") {
39400
+ rightOffset = rightStickyColumnWidths.reduce((acc, width) => acc + width, 0);
39401
+ const nextCol = Object.assign(Object.assign({}, col), { stickyOffset: rightOffset });
39402
+ rightStickyColumnWidths.shift();
39403
+ return nextCol;
39404
+ }
39405
+ return col;
39406
+ });
39407
+ };
39378
39408
  const filterComponentMap = {
39379
39409
  categorical: CategoricalFilter,
39380
39410
  datetime: DatetimeFilter,
@@ -39392,12 +39422,26 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39392
39422
  return Object.assign(Object.assign({}, col), { Filter: filterComponentMap[col.filter] });
39393
39423
  });
39394
39424
  };
39425
+ const createActionColumn = (actions, accessor, sticky, disableSelectAll = false) => {
39426
+ const width = actions.includes(Actions.SELECT) ? 52 : actions.length * 24 + 24;
39427
+ return {
39428
+ Cell: ActionCell,
39429
+ Header: actions.includes(Actions.SELECT) && !disableSelectAll ? SelectHeader : "",
39430
+ accessor: accessor || "actions",
39431
+ actions,
39432
+ disableSortBy: true,
39433
+ maxWidth: width,
39434
+ minWidth: actions.includes(Actions.SELECT) ? 52 : 48,
39435
+ sticky: sticky || null,
39436
+ width
39437
+ };
39438
+ };
39395
39439
  const cells = {
39396
39440
  DATETIME: DatetimeCell,
39397
39441
  EDIT_INPUT: EditInputCell,
39398
39442
  EDIT_SELECT: EditSelectCell
39399
39443
  };
39400
- const createItemData$1 = memoizeOne$1((width, currentEditCell, headerGroups, rows, prepareRow, getItem, totalColumnsWidth, onClickRow, throttledClickRow, backgroundColor, mappedColumns) => ({
39444
+ const createItemData$1 = memoizeOne$1((width, currentEditCell, headerGroups, rows, prepareRow, getItem, totalColumnsWidth, onClickRow, throttledClickRow, backgroundColor, mappedColumns, rowHeight) => ({
39401
39445
  backgroundColor,
39402
39446
  currentEditCell,
39403
39447
  getItem,
@@ -39406,12 +39450,14 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39406
39450
  onClickRow,
39407
39451
  prepareRow,
39408
39452
  rows,
39453
+ rowHeight,
39409
39454
  throttledClickRow,
39410
39455
  totalColumnsWidth,
39411
39456
  width
39412
39457
  }));
39413
- const Table = React$1.forwardRef(({ allowHiding, backgroundColor, className, columns, data: data2, getItem, initialSort = [], itemCount, maxRows, onAction, onChange: onChange2, onClickRow, onItemsRendered, onFilter, onSort, showTableOptions, style, tableOptionsStyle }, ref) => {
39458
+ const Table = React$1.forwardRef(({ allowHiding, actions, backgroundColor, className, columns, data: data2, getItem, initialSort = [], itemCount, maxRows, onAction, onChange: onChange2, onClickRow, onItemsRendered, onFilter, onSort, rowHeight, showTableOptions, style, tableOptionsStyle }, ref) => {
39414
39459
  const [currentSortBy, setCurrentSortBy] = React$1.useState(initialSort);
39460
+ const tableRowHeight = rowHeight || DEFAULT_ROW_HEIGHT;
39415
39461
  React$1.useEffect(
39416
39462
  () => {
39417
39463
  setCurrentSortBy(initialSort);
@@ -39441,7 +39487,14 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39441
39487
  throttledSetEditCell([Number(cell[0]), cell[1]]);
39442
39488
  };
39443
39489
  const infiniteData = React$1.useMemo(() => Array(itemCount).fill(0), [itemCount]);
39444
- const mappedColumns = React$1.useMemo(() => appendFilterComponents(orderStickyCols(columns)), [columns]);
39490
+ const mappedColumns = React$1.useMemo(() => {
39491
+ let processedColumns = columns;
39492
+ if (actions && actions.length > 0) {
39493
+ const actionColumn = createActionColumn(actions);
39494
+ processedColumns = [...columns, actionColumn];
39495
+ }
39496
+ return appendStickyOffsets(appendFilterComponents(orderStickyCols(processedColumns)));
39497
+ }, [columns, actions]);
39445
39498
  const hasFixedColumns = React$1.useMemo(() => mappedColumns.some((column) => "sticky" in column), [mappedColumns]);
39446
39499
  const totalColumnsWidth = React$1.useMemo(() => mappedColumns.reduce((acc, column) => acc + (parseInt(column.width) || 150), 0), [mappedColumns]);
39447
39500
  const filterTypes = React$1.useMemo(() => ({
@@ -39497,11 +39550,15 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39497
39550
  const showHeaderCellButtonContainer = showSort || showFilter || showOptions;
39498
39551
  return React$1.createElement(
39499
39552
  HeaderCell,
39500
- Object.assign({}, headerProps, { key: `col-${gidx}-${cidx}`, style: Object.assign(Object.assign({}, headerProps.style), {
39553
+ Object.assign({}, headerProps, { key: `col-${gidx}-${cidx}`, style: Object.assign(Object.assign(Object.assign(Object.assign({}, headerProps.style), {
39501
39554
  maxWidth: col.maxWidth,
39502
39555
  // If width calc has messed up then use the raw width from the column
39503
39556
  width: headerProps.style.width === "NaNpx" ? mappedColumns[cidx].width : headerProps.style.width
39504
- }) }),
39557
+ }), col.sticky === "left" && typeof col.stickyOffset === "number" ? {
39558
+ left: `${col.stickyOffset}px`
39559
+ } : {}), col.sticky === "right" && typeof col.stickyOffset === "number" ? {
39560
+ right: `${col.stickyOffset}px`
39561
+ } : {}) }),
39505
39562
  jsxRuntimeExports.jsxs(HeaderTooltipContainer, { isPrimitiveHeader: typeof headerContent === "string", children: [jsxRuntimeExports.jsx(HeaderContentWrapper, Object.assign({}, sortProps, { isPrimitiveHeader: typeof headerContent === "string", title: typeof headerContent === "string" ? headerContent : "", children: headerContent })), col.tooltip && jsxRuntimeExports.jsx(Tooltip, { content: col.tooltip, children: jsxRuntimeExports.jsx(TooltipIcon, { icon: faCircleQuestion }) })] }),
39506
39563
  showHeaderCellButtonContainer && jsxRuntimeExports.jsxs(HeaderCellButtonContainer, { children: [jsxRuntimeExports.jsxs(HeaderIconsWrapper, { children: [showSort && jsxRuntimeExports.jsx(HeaderIconWrapper, { children: jsxRuntimeExports.jsx(SortIcon, Object.assign({}, sortProps, { className: "tableSortArrow", icon: getSortIcon(col.isSorted, col.isSortedDesc), isSorted: col.isSorted })) }), showFilter ? jsxRuntimeExports.jsx(FilterContainer, { col }) : null, showOptions && jsxRuntimeExports.jsx(OptionsMenu, { allColumns, allowColumnHiding: allowHiding, numVisibleColumns, resetResizing, setAllFilters, style: tableOptionsStyle })] }), jsxRuntimeExports.jsx(ResizeBorder, Object.assign({}, resizerProps))] })
39507
39564
  );
@@ -39510,28 +39567,15 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
39510
39567
  // eslint-disable-next-line react-hooks/exhaustive-deps
39511
39568
  useDeepCompare([tableProps, totalColumnsWidth, headerGroups])
39512
39569
  );
39513
- return jsxRuntimeExports.jsx(Wrapper$2, Object.assign({}, getTableProps(), { "$hasMaxRows": !!maxRows, className: `${className} ${hasFixedColumns ? "sticky" : ""}`, style: Object.assign({ height: maxRows ? (Math.min(rows.length, maxRows) + 1) * ROW_HEIGHT : "100%" }, style), children: jsxRuntimeExports.jsx(AutoSizer, { children: ({ height, width }) => {
39514
- return jsxRuntimeExports.jsx(StyledFixedSizeList, { height, innerElementType: renderTable, itemCount: itemCount || rows.length, itemData: createItemData$1(width, currentEditCell, headerGroups, rows, prepareRow, getItem, totalColumnsWidth, onClickRow, throttledClickRow, backgroundColor, mappedColumns), itemSize: ROW_HEIGHT, onItemsRendered, style: {
39570
+ return jsxRuntimeExports.jsx(Wrapper$2, Object.assign({}, getTableProps(), { "$hasMaxRows": !!maxRows, className: `${className} ${hasFixedColumns ? "sticky" : ""}`, style: Object.assign({ height: maxRows ? (Math.min(rows.length, maxRows) + 1) * tableRowHeight : "100%" }, style), children: jsxRuntimeExports.jsx(AutoSizer, { children: ({ height, width }) => {
39571
+ return jsxRuntimeExports.jsx(StyledFixedSizeList, { height, innerElementType: renderTable, itemCount: itemCount || rows.length, itemData: createItemData$1(width, currentEditCell, headerGroups, rows, prepareRow, getItem, totalColumnsWidth, onClickRow, throttledClickRow, backgroundColor, mappedColumns, tableRowHeight), itemSize: tableRowHeight, onItemsRendered, style: {
39515
39572
  overflowX: width < totalColumnsWidth ? "auto" : "hidden",
39516
- overflowY: height < (rows.length + 1) * ROW_HEIGHT ? "auto" : "hidden"
39573
+ overflowY: height < (rows.length + 1) * tableRowHeight ? "auto" : "hidden"
39517
39574
  }, width, children: RenderRow }, "table-list");
39518
39575
  } }) }));
39519
39576
  });
39520
39577
  Table.displayName = "Table";
39521
- Table.ActionColumn = (actions, accessor, sticky, disableSelectAll = false) => {
39522
- const width = actions.includes(Actions.SELECT) ? 52 : actions.length * 24 + 24;
39523
- return {
39524
- Cell: ActionCell,
39525
- Header: actions.includes(Actions.SELECT) && !disableSelectAll ? SelectHeader : "",
39526
- accessor: accessor || "actions",
39527
- actions,
39528
- disableSortBy: true,
39529
- maxWidth: width,
39530
- minWidth: actions.includes(Actions.SELECT) ? 52 : 48,
39531
- sticky: sticky || null,
39532
- width
39533
- };
39534
- };
39578
+ Table.ActionColumn = createActionColumn;
39535
39579
  Table.Actions = Actions;
39536
39580
  Table.cells = cells;
39537
39581
  styled.div`
@@ -18,6 +18,7 @@ limitations under the License.
18
18
  from contextvars import ContextVar
19
19
  from datetime import datetime
20
20
 
21
+ from pydantic import ConfigDict
21
22
  from typing_extensions import TypedDict
22
23
 
23
24
  from dara.core.base_definitions import DaraBaseModel as BaseModel
@@ -60,6 +61,9 @@ class UserData(BaseModel):
60
61
  identity_email: str | None = None
61
62
  groups: list[str] | None = []
62
63
 
64
+ # allow extra for more flexibility in custom oidc configs
65
+ model_config = ConfigDict(extra='allow')
66
+
63
67
  @classmethod
64
68
  def from_token_data(cls, token_data: TokenData):
65
69
  return cls(
@@ -65,6 +65,7 @@ class OIDCAuthConfig(BaseAuthConfig):
65
65
  - SSO_EXTRA_AUDIENCE - if set, extra audiences to verify against the ID token in addition to `sso_client_id`
66
66
  - SSO_SCOPES - space-separated list of scopes to request from the identity provider, defaults to `openid`
67
67
  - SSO_JWT_ALGO - algorithm to use for verifying IDP-provided JWTs, defaults to `ES256`
68
+ - SSO_USE_USERINFO - if set to `true`, fetch additional claims from the userinfo endpoint when an access token is available
68
69
  """
69
70
 
70
71
  # NOTE: the config follows OIDC specification, but makes a few concessions
@@ -209,29 +210,84 @@ class OIDCAuthConfig(BaseAuthConfig):
209
210
  state = self.generate_state(redirect_to=body.redirect_to)
210
211
  return RedirectResponse(redirect_uri=self.get_authorization_url(state))
211
212
 
212
- def extract_user_data_from_id_token(self, claims: IdTokenClaims) -> UserData:
213
+ async def fetch_userinfo(self, access_token: str) -> dict | None:
213
214
  """
214
- Extract user data from ID token claims.
215
+ Fetch user information from the OIDC userinfo endpoint.
216
+
217
+ Per OpenID Connect Core 1.0 Section 5.3, the userinfo endpoint returns claims
218
+ about the authenticated user. This is useful when the ID token doesn't contain
219
+ all required claims.
220
+
221
+ :param access_token: The access token to authenticate the request
222
+ :return: Dictionary of userinfo claims, or None if the request fails
223
+ """
224
+ userinfo_endpoint = self.discovery.userinfo_endpoint
225
+ if not userinfo_endpoint:
226
+ dev_logger.warning('Userinfo endpoint not available in OIDC discovery')
227
+ return None
228
+
229
+ try:
230
+ response = await self.client.get(
231
+ userinfo_endpoint,
232
+ headers={'Authorization': f'Bearer {access_token}'},
233
+ timeout=10,
234
+ )
235
+ response.raise_for_status()
236
+ return response.json()
237
+ except httpx.HTTPStatusError as e:
238
+ dev_logger.warning(
239
+ f'Failed to fetch userinfo: HTTP {e.response.status_code}',
240
+ )
241
+ return None
242
+ except httpx.RequestError as e:
243
+ dev_logger.warning(f'Failed to fetch userinfo: {e}')
244
+ return None
245
+
246
+ def extract_user_data(self, claims: IdTokenClaims, userinfo: dict | None = None) -> UserData:
247
+ """
248
+ Extract user data from ID token claims and optional userinfo response.
215
249
 
216
250
  Override this method in subclasses to handle provider-specific claim structures.
217
251
  The default implementation uses standard OIDC claims, with support for the
218
- non-standard 'identity' claim.
252
+ non-standard 'identity' claim. When userinfo is provided and SSO_USE_USERINFO
253
+ is enabled, userinfo claims take precedence over ID token claims.
219
254
 
220
255
  :param claims: Decoded ID token claims
256
+ :param userinfo: Optional userinfo response from the userinfo endpoint
221
257
  :return: UserData extracted from the claims
222
258
  """
223
- # Check for non-standard 'identity' claim (Causalens IDP)
224
- # This is a nested object with id, name, email fields
225
- identity_claim = getattr(claims, 'identity', None)
226
- if isinstance(identity_claim, dict):
227
- identity_id = identity_claim.get('id') or claims.sub
228
- identity_name = identity_claim.get('name')
229
- identity_email = identity_claim.get('email') or claims.email
259
+ oidc_settings = get_oidc_settings()
260
+
261
+ # When userinfo is provided and use_userinfo is enabled, prefer userinfo claims
262
+ if userinfo and oidc_settings.use_userinfo:
263
+ # userinfo 'sub' must match id_token 'sub' per OIDC spec
264
+ identity_id = userinfo.get('sub') or claims.sub
265
+ identity_email = userinfo.get('email') or claims.email
266
+ identity_name = (
267
+ userinfo.get('name')
268
+ or userinfo.get('preferred_username')
269
+ or userinfo.get('nickname')
270
+ or (
271
+ f'{userinfo.get("given_name", "")} {userinfo.get("family_name", "")}'.strip()
272
+ if userinfo.get('given_name') or userinfo.get('family_name')
273
+ else None
274
+ )
275
+ )
276
+ groups = userinfo.get('groups') or claims.groups
230
277
  else:
231
- # Standard OIDC: use 'sub' as the identity ID
232
- identity_id = claims.sub
233
- identity_email = claims.email
234
- identity_name = None
278
+ # Check for non-standard 'identity' claim (Causalens IDP)
279
+ # This is a nested object with id, name, email fields
280
+ identity_claim = getattr(claims, 'identity', None)
281
+ if isinstance(identity_claim, dict):
282
+ identity_id = identity_claim.get('id') or claims.sub
283
+ identity_name = identity_claim.get('name')
284
+ identity_email = identity_claim.get('email') or claims.email
285
+ else:
286
+ # Standard OIDC: use 'sub' as the identity ID
287
+ identity_id = claims.sub
288
+ identity_email = claims.email
289
+ identity_name = None
290
+ groups = claims.groups
235
291
 
236
292
  # Fall back to standard claims for name if not set
237
293
  if not identity_name:
@@ -252,7 +308,7 @@ class OIDCAuthConfig(BaseAuthConfig):
252
308
  identity_id=identity_id,
253
309
  identity_name=identity_name,
254
310
  identity_email=identity_email,
255
- groups=claims.groups,
311
+ groups=groups,
256
312
  )
257
313
 
258
314
  def verify_token(self, token: str) -> TokenData:
@@ -291,7 +347,7 @@ class OIDCAuthConfig(BaseAuthConfig):
291
347
  claims = decode_id_token(token)
292
348
 
293
349
  # Extract user data (can be overridden for provider-specific claim structures)
294
- user_data = self.extract_user_data_from_id_token(claims)
350
+ user_data = self.extract_user_data(claims)
295
351
 
296
352
  # Verify user has access based on groups
297
353
  self.verify_user_access(user_data)
@@ -390,6 +446,8 @@ class OIDCAuthConfig(BaseAuthConfig):
390
446
  :return: Tuple of (new_session_token, new_refresh_token)
391
447
  :raises HTTPException: If the refresh fails
392
448
  """
449
+ oidc_settings = get_oidc_settings()
450
+
393
451
  # Request new tokens from the IDP
394
452
  oidc_tokens = await get_token_from_idp(
395
453
  self,
@@ -406,8 +464,13 @@ class OIDCAuthConfig(BaseAuthConfig):
406
464
  # Decode and verify the new ID token
407
465
  claims = decode_id_token(oidc_tokens.id_token)
408
466
 
467
+ # Fetch userinfo if enabled and we have an access token
468
+ userinfo = None
469
+ if oidc_settings.use_userinfo and oidc_tokens.access_token:
470
+ userinfo = await self.fetch_userinfo(oidc_tokens.access_token)
471
+
409
472
  # Extract user data from claims
410
- user_data = self.extract_user_data_from_id_token(claims)
473
+ user_data = self.extract_user_data(claims, userinfo=userinfo)
411
474
 
412
475
  # Verify user still has access
413
476
  self.verify_user_access(user_data)
@@ -100,8 +100,13 @@ async def sso_callback(
100
100
  # Decode and verify the ID token
101
101
  claims = decode_id_token(oidc_tokens.id_token)
102
102
 
103
+ # Fetch userinfo if enabled and we have an access token
104
+ userinfo = None
105
+ if oidc_settings.use_userinfo and oidc_tokens.access_token:
106
+ userinfo = await auth_config.fetch_userinfo(oidc_tokens.access_token)
107
+
103
108
  # Extract user data from claims (handles both standard OIDC and Causalens identity claim)
104
- user_data = auth_config.extract_user_data_from_id_token(claims)
109
+ user_data = auth_config.extract_user_data(claims, userinfo=userinfo)
105
110
 
106
111
  # Verify user has access based on groups
107
112
  auth_config.verify_user_access(user_data)
@@ -24,6 +24,8 @@ class OIDCSettings(BaseSettings):
24
24
  verify_audience: bool = False
25
25
  extra_audience: list[str] | None = None
26
26
  allowed_identity_id: str | None = None
27
+ use_userinfo: bool = False
28
+ """If True, fetch additional claims from the userinfo endpoint when an access token is available."""
27
29
 
28
30
  model_config = SettingsConfigDict(env_file='.env', extra='allow', env_prefix='sso_')
29
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dara-core
3
- Version: 1.23.0a1
3
+ Version: 1.23.2a1
4
4
  Summary: Dara Framework Core
5
5
  Home-page: https://dara.causalens.com/
6
6
  License: Apache-2.0
@@ -21,10 +21,10 @@ Requires-Dist: cachetools (>=5.0.0)
21
21
  Requires-Dist: certifi (>=2024.7.4)
22
22
  Requires-Dist: click (>=8.1.3,<9.0.0)
23
23
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
24
- Requires-Dist: create-dara-app (==1.23.0-alpha.1)
24
+ Requires-Dist: create-dara-app (==1.23.2-alpha.1)
25
25
  Requires-Dist: croniter (>=6.0.0,<7.0.0)
26
26
  Requires-Dist: cryptography (>=42.0.4)
27
- Requires-Dist: dara-components (==1.23.0-alpha.1) ; extra == "all"
27
+ Requires-Dist: dara-components (==1.23.2-alpha.1) ; extra == "all"
28
28
  Requires-Dist: exceptiongroup (>=1.1.3,<2.0.0)
29
29
  Requires-Dist: fastapi (>=0.115.0,<0.121.0)
30
30
  Requires-Dist: fastapi_vite_dara (==0.4.0)
@@ -55,7 +55,7 @@ Description-Content-Type: text/markdown
55
55
 
56
56
  # Dara Application Framework
57
57
 
58
- <img src="https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/dara_light.svg?raw=true">
58
+ <img src="https://github.com/causalens/dara/blob/v1.23.2-alpha.1/img/dara_light.svg?raw=true">
59
59
 
60
60
  ![Master tests](https://github.com/causalens/dara/actions/workflows/tests.yml/badge.svg?branch=master)
61
61
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -100,7 +100,7 @@ source .venv/bin/activate
100
100
  dara start
101
101
  ```
102
102
 
103
- ![Dara App](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/components_gallery.png?raw=true)
103
+ ![Dara App](https://github.com/causalens/dara/blob/v1.23.2-alpha.1/img/components_gallery.png?raw=true)
104
104
 
105
105
  Note: `pip` installation uses [PEP 660](https://peps.python.org/pep-0660/) `pyproject.toml`-based editable installs which require `pip >= 21.3` and `setuptools >= 64.0.0`. You can upgrade both with:
106
106
 
@@ -117,9 +117,9 @@ Explore some of our favorite apps - a great way of getting started and getting t
117
117
 
118
118
  | Dara App | Description |
119
119
  | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | ![Large Language Model](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
121
- | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/plot_interactivity.png?raw=true) | Demonstrates how to enable the user to interact with plots, trigger actions based on clicks, mouse movements and other interactions with `Bokeh` or `Plotly` plots |
122
- | ![Graph Editor](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/img/graph_viewer.png?raw=true) | Demonstrates how to use the `CausalGraphViewer` component to display your graphs or networks, customising the displayed information through colors and tooltips, and updating the page based on user interaction. |
120
+ | ![Large Language Model](https://github.com/causalens/dara/blob/v1.23.2-alpha.1/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
121
+ | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.23.2-alpha.1/img/plot_interactivity.png?raw=true) | Demonstrates how to enable the user to interact with plots, trigger actions based on clicks, mouse movements and other interactions with `Bokeh` or `Plotly` plots |
122
+ | ![Graph Editor](https://github.com/causalens/dara/blob/v1.23.2-alpha.1/img/graph_viewer.png?raw=true) | Demonstrates how to use the `CausalGraphViewer` component to display your graphs or networks, customising the displayed information through colors and tooltips, and updating the page based on user interaction. |
123
123
 
124
124
  Check out our [App Gallery](https://dara.causalens.com/gallery) for more inspiration!
125
125
 
@@ -146,9 +146,9 @@ And the supporting UI packages and tools.
146
146
  - `ui-utils` - miscellaneous utility functions
147
147
  - `ui-widgets` - widget components
148
148
 
149
- More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/CONTRIBUTING.md) file.
149
+ More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.23.2-alpha.1/CONTRIBUTING.md) file.
150
150
 
151
151
  ## License
152
152
 
153
- Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.23.0-alpha.1/LICENSE).
153
+ Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.23.2-alpha.1/LICENSE).
154
154
 
@@ -1,7 +1,7 @@
1
1
  dara/core/__init__.py,sha256=yTp-lXT0yy9XqLGYWlmjPgFG5g2eEg2KhKo8KheTHoo,1408
2
2
  dara/core/_assets/__init__.py,sha256=13vMoWHvl1zcFcjNHh8lbTwWOvu4f7krYSco978qxwM,723
3
3
  dara/core/_assets/auto_js/dara.core.css,sha256=yT3PKpi2sKI2-kQIF8xtVbTPQqgpK7-Ua7tfzDPuSsI,4095881
4
- dara/core/_assets/auto_js/dara.core.umd.cjs,sha256=T9MOYSRnuHnraSboQ7NcfodV8vrBkNGISoCZQCuc4Ec,5163725
4
+ dara/core/_assets/auto_js/dara.core.umd.cjs,sha256=JBxFuCmrugsMdISIVkEEWbdCSht8gydVvwt2Vksnmyw,5166175
5
5
  dara/core/_assets/auto_js/react-dom.development.js,sha256=vR2Fq5LXMKS5JsTo2CMl6oGCJT8scJV2wXSteTtx8aE,1077040
6
6
  dara/core/_assets/auto_js/react-is.development.js,sha256=2IRgmaphdMq6wx2MbsmVUQ0UwapGETNjgano3XEkGcc,7932
7
7
  dara/core/_assets/auto_js/react-query.development.js,sha256=lI2fTKMvWmjbagGQaVtZYr51_-C_U_so064JwetuDS0,130366
@@ -12,12 +12,12 @@ dara/core/actions.py,sha256=rC5Tu79AFNWMv0CJuchBnoy6pETIFh_1RTSqxrolArI,947
12
12
  dara/core/auth/__init__.py,sha256=HJoYIVPzbpwzN_RUHjGpSJj4o5TmHz9yFyZGiRiObCk,906
13
13
  dara/core/auth/base.py,sha256=NJmUJqA-W8AVKIQbX_0BoHoZqtU1Iz6cJ16RKdcdIyU,3642
14
14
  dara/core/auth/basic.py,sha256=sglIaogCslG2HlDMjFsaaJhOJeXUW-QQLTIYPaUPxAU,4927
15
- dara/core/auth/definitions.py,sha256=OzJshDLuut8MaM_pbfyQFkSlUuMNBWNszA57jRXF0Cg,3848
15
+ dara/core/auth/definitions.py,sha256=ZZMXJbMFY42Q5YaJCMwZT0bivnxq7TaIoug7uYqAGzE,3988
16
16
  dara/core/auth/oidc/__init__.py,sha256=UWdhFvDqLCoILaKVWbmrrJgiMgg9wlVZgCxRvf_HGHM,65
17
- dara/core/auth/oidc/config.py,sha256=5rDxbr3wVZdjIz8USYNeUmKwKfeucHtLgfXwM3_weaE,20835
17
+ dara/core/auth/oidc/config.py,sha256=XdiJSC2-9g8fqM-FkmU0c079VqnrApVv-MKwEBwk50s,23753
18
18
  dara/core/auth/oidc/definitions.py,sha256=KLlUl2Y7n6prX0JSAwPde6J43iMs6uUY07OG7kumRPw,17568
19
- dara/core/auth/oidc/routes.py,sha256=uvNHYUieaXy9hu6MYe_5eZvflX5TmFaIv6EGpCRP-dw,5335
20
- dara/core/auth/oidc/settings.py,sha256=k1CKDTUKSFoCEp1ASI_R5L_lsNkFzjfAymMMIMmI1AE,2097
19
+ dara/core/auth/oidc/routes.py,sha256=wlYp8VZP4C4BFHqJPPYHi3PdLd8SetdG2CtG3B4laBk,5579
20
+ dara/core/auth/oidc/settings.py,sha256=wU6fmkqhTNzMNFUxizFaaavH2cKuXeqRIzMi3TpwLhA,2233
21
21
  dara/core/auth/oidc/utils.py,sha256=IL17tStRNpkAQI3M17nrzGeb4xr47zSLPo4wT7LMwBQ,5145
22
22
  dara/core/auth/routes.py,sha256=dtOxpFotnt4XQ4spW3mbyM7ThYRvfIA_oRK5X5lyYHg,7256
23
23
  dara/core/auth/utils.py,sha256=_aHZ98qMJ0VLE9Zfvj5biPARhe973_eyTx2qxnufzRE,7290
@@ -134,8 +134,8 @@ dara/core/visual/themes/__init__.py,sha256=aM4mgoIYo2neBSw5FRzswsht7PUKjLthiHLmF
134
134
  dara/core/visual/themes/dark.py,sha256=QazCRDqh_SCOyQhdwMkH1wbHf301oL7gCFj91plbLww,2020
135
135
  dara/core/visual/themes/definitions.py,sha256=dtET2YUlwXkO6gJ23MqSb8gIq-LxJ343CWsgueWSifM,2787
136
136
  dara/core/visual/themes/light.py,sha256=dtHb6Q1HOb5r_AvJfe0vZajikVc-GnBEUrGsTcI5MHA,2022
137
- dara_core-1.23.0a1.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
138
- dara_core-1.23.0a1.dist-info/METADATA,sha256=WH9TER9_ycRXWaU40zxhvD5teCWhUbExsBay21YHW_w,7598
139
- dara_core-1.23.0a1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
140
- dara_core-1.23.0a1.dist-info/entry_points.txt,sha256=nAT9o1kJCmTK1saDh29PFGFD6cbxDDDjTj31HDEDwfU,197
141
- dara_core-1.23.0a1.dist-info/RECORD,,
137
+ dara_core-1.23.2a1.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
138
+ dara_core-1.23.2a1.dist-info/METADATA,sha256=X5eVDXyUa0Ikj_AGLfwFOIrMgxyI2NeLL1rvPG-YZB8,7598
139
+ dara_core-1.23.2a1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
140
+ dara_core-1.23.2a1.dist-info/entry_points.txt,sha256=nAT9o1kJCmTK1saDh29PFGFD6cbxDDDjTj31HDEDwfU,197
141
+ dara_core-1.23.2a1.dist-info/RECORD,,