Skip to content

Commit d699670

Browse files
Merge pull request KelvinTegelaar#58 from CyberDrain/dev
Dev to release Synced from CyberDrain/CIPP@75e5558
1 parent 878d44d commit d699670

46 files changed

Lines changed: 5079 additions & 448 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.npmrc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Supply-chain hardening for CIPP
2-
# This file is honored by BOTH npm and yarn (yarn classic reads .npmrc).
2+
# npm honors this file. yarn classic does NOT read `ignore-scripts` from
3+
# .npmrc (only registry/auth keys); the yarn equivalents live in .yarnrc.
34
# Any change here should be reviewed for CI/CD impact.
45

56
# Refuse to execute package lifecycle scripts (pre/postinstall, prepare, etc.)
@@ -12,13 +13,13 @@ ignore-scripts=true
1213
# CI / contributor installs to a malicious mirror.
1314
registry=https://registry.npmjs.org/
1415

15-
# Require integrity hashes (sha512) to match the lockfile on install.
16-
# npm honors this directly; yarn classic always verifies lockfile integrity
17-
# but this makes the intent explicit.
16+
# Severity threshold at which `npm audit` exits non-zero. (Integrity hashes
17+
# are always verified against the lockfile on install; that needs no flag.)
1818
audit-level=high
1919

20-
# Don't auto-save changes to the lockfile from arbitrary install commands.
21-
# Lockfile edits should only happen via Dependabot PRs or explicit upgrades.
20+
# Write exact versions to package.json when adding deps via `npm install <pkg>`,
21+
# so a future lockfile regeneration cannot drift to a newer release.
22+
# npm-only; the yarn equivalent is `yarn add --exact`.
2223
save-exact=true
2324

2425
# Disable funding/notifier noise so CI logs only show real signal.

.yarnrc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Supply-chain hardening for CIPP (yarn 1 / classic)
22
#
3-
# This complements .npmrc — yarn 1 honors `ignore-scripts` from .npmrc, but
4-
# we set the per-command equivalents here as defense in depth so the
5-
# protection survives even if .npmrc is missing or ignored.
3+
# This complements .npmrc but is not redundant with it: yarn 1 does NOT
4+
# honor `ignore-scripts` from .npmrc, so for yarn installs these per-command
5+
# flags are the protection, not defense in depth.
66

77
# Refuse to execute lifecycle scripts on `yarn install` / `yarn add` /
88
# `yarn upgrade`. Mirrors `ignore-scripts=true` in .npmrc.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "cipp",
3-
"version": "10.6.4",
3+
"version": "10.7.0",
44
"author": "CIPP Contributors",
55
"homepage": "https://cipp.app/",
66
"bugs": {
7-
"url": "https://github.com/KelvinTegelaar/CIPP/issues"
7+
"url": "https://github.com/CyberDrain/CIPP/issues"
88
},
99
"license": "AGPL-3.0",
1010
"engines": {

public/assets/logos/sharepoint.svg

Lines changed: 42 additions & 0 deletions
Loading

public/assets/logos/teams.svg

Lines changed: 24 additions & 0 deletions
Loading

public/version.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"version": "10.6.4"
3-
}
2+
"version": "10.7.0"
3+
}

src/components/CippCards/CippChartCard.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export const CippChartCard = ({
109109
const [barSeries, setBarSeries] = useState([]);
110110
const chartOptions = useChartOptions(labels, chartType);
111111
chartSeries = chartSeries.filter((item) => item !== null);
112-
const calculatedTotal = chartSeries.reduce((acc, value) => acc + value, 0);
112+
// Round to 2 decimals - summing fractional series values accumulates floating-point
113+
// artifacts (e.g. 175.73000000000002). Integer series are unaffected.
114+
const calculatedTotal = Math.round(chartSeries.reduce((acc, value) => acc + value, 0) * 100) / 100;
113115
const total = customTotal !== undefined ? customTotal : calculatedTotal;
114116
useEffect(() => {
115117
if (chartType === "bar") {

src/components/CippComponents/CippApplicationDeployDrawer.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,13 @@ export const CippApplicationDeployDrawer = ({
498498

499499
{/* Install Options */}
500500
<Grid size={{ xs: 12 }}>
501+
<CippFormComponent
502+
type="switch"
503+
label="Install as system"
504+
name="InstallAsSystem"
505+
formControl={formControl}
506+
defaultValue={true}
507+
/>
501508
<CippFormComponent
502509
type="switch"
503510
label="Mark for Uninstallation"
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { useMemo, useState } from 'react'
2+
import {
3+
Alert,
4+
AlertTitle,
5+
Button,
6+
Chip,
7+
Dialog,
8+
DialogActions,
9+
DialogContent,
10+
DialogTitle,
11+
Divider,
12+
Skeleton,
13+
Stack,
14+
Typography,
15+
} from '@mui/material'
16+
import { Search } from '@mui/icons-material'
17+
import { useForm } from 'react-hook-form'
18+
import CippFormComponent from './CippFormComponent'
19+
import { CippDataTable } from '../CippTable/CippDataTable'
20+
import { ApiGetCall } from '../../api/ApiCall'
21+
22+
const SITE_ROOT = '__siteRoot__'
23+
const SITE_ROOT_OPTION = { label: 'Site root (whole site)', value: SITE_ROOT }
24+
25+
// Stable empty fallback: CippDataTable syncs its `data` prop by reference.
26+
const NO_PATHS = []
27+
28+
const optionValue = (x) => (x && typeof x === 'object' && 'value' in x ? x.value : x)
29+
30+
// Explains how one user ends up with access to a site or library, rather than listing who holds
31+
// permissions. Answers the question the permission lists cannot: a group holding Edit says
32+
// nothing about whether this particular person is in it.
33+
export const CippCheckUserAccessDialog = ({
34+
row,
35+
tenantFilter,
36+
drawerVisible,
37+
setDrawerVisible,
38+
}) => {
39+
const siteRow = Array.isArray(row) ? row[0] : row
40+
const siteUrl = siteRow?.webUrl
41+
const siteId = siteRow?.siteId
42+
const tenant = siteRow?.Tenant ?? tenantFilter
43+
const isOpen = !!drawerVisible
44+
45+
const formControl = useForm({ defaultValues: { user: null, scope: SITE_ROOT_OPTION } })
46+
const selectedUser = formControl.watch('user')
47+
const selectedScope = formControl.watch('scope')
48+
49+
// The check only runs once asked for, so changing the user does not fire a request per keystroke.
50+
const [query, setQuery] = useState(null)
51+
52+
const libraries = ApiGetCall({
53+
url: '/api/ListSiteLibraries',
54+
data: { SiteId: siteId, SiteUrl: siteUrl, tenantFilter: tenant },
55+
queryKey: `SiteLibraries-${siteId ?? siteUrl}`,
56+
waiting: isOpen && !!siteUrl,
57+
})
58+
59+
const scopeOptions = useMemo(() => {
60+
const libs = Array.isArray(libraries.data?.Results) ? libraries.data.Results : []
61+
return [
62+
SITE_ROOT_OPTION,
63+
...libs.map((library) => ({ label: library.Title, value: library.Id })),
64+
]
65+
}, [libraries.data])
66+
67+
const access = ApiGetCall({
68+
url: '/api/ListSiteUserAccess',
69+
data: query ?? {},
70+
queryKey: `SiteUserAccess-${siteUrl}-${query?.ListId || 'root'}-${query?.UserPrincipalName}`,
71+
waiting: isOpen && !!query,
72+
})
73+
74+
const runCheck = () => {
75+
const upn = optionValue(selectedUser)
76+
if (!upn) return
77+
const scopeId = optionValue(selectedScope)
78+
setQuery({
79+
tenantFilter: tenant,
80+
SiteUrl: siteUrl,
81+
ListId: !scopeId || scopeId === SITE_ROOT ? '' : scopeId,
82+
UserPrincipalName: upn,
83+
})
84+
}
85+
86+
const result = access.data?.Results
87+
const data = typeof result === 'object' && result !== null ? result : null
88+
const loadError = typeof result === 'string' ? result : null
89+
const paths = data?.Paths ?? NO_PATHS
90+
const realPaths = paths.filter((p) => p.GrantsRealAccess)
91+
const limitedOnly = paths.length > 0 && realPaths.length === 0
92+
93+
return (
94+
<Dialog fullWidth maxWidth="md" open={isOpen} onClose={() => setDrawerVisible(false)}>
95+
<DialogTitle>
96+
Check User Access{siteRow?.displayName ? ` — ${siteRow.displayName}` : ''}
97+
</DialogTitle>
98+
<DialogContent dividers>
99+
<Stack spacing={2}>
100+
<Typography variant="body2" color="text.secondary">
101+
Shows every route by which a user can reach this site or one of its libraries, and what
102+
each route grants. Group memberships are resolved, including nested groups, so this
103+
answers whether someone actually has access rather than who holds the permissions.
104+
</Typography>
105+
106+
<CippFormComponent
107+
type="autoComplete"
108+
name="user"
109+
label="User"
110+
multiple={false}
111+
creatable={false}
112+
formControl={formControl}
113+
api={{
114+
url: '/api/ListGraphRequest',
115+
data: {
116+
Endpoint: 'users',
117+
$select: 'id,displayName,userPrincipalName',
118+
$top: 999,
119+
$count: true,
120+
},
121+
queryKey: 'ListUsersAutoComplete',
122+
dataKey: 'Results',
123+
labelField: (user) => `${user.displayName} (${user.userPrincipalName})`,
124+
valueField: 'userPrincipalName',
125+
showRefresh: true,
126+
}}
127+
/>
128+
129+
<CippFormComponent
130+
type="autoComplete"
131+
name="scope"
132+
label="Document Library"
133+
multiple={false}
134+
creatable={false}
135+
formControl={formControl}
136+
options={scopeOptions}
137+
isFetching={libraries.isFetching}
138+
/>
139+
140+
<Stack direction="row" justifyContent="flex-end">
141+
<Button
142+
variant="contained"
143+
size="small"
144+
startIcon={<Search />}
145+
disabled={!optionValue(selectedUser) || access.isFetching}
146+
onClick={runCheck}
147+
>
148+
Check Access
149+
</Button>
150+
</Stack>
151+
152+
{loadError && <Alert severity="error">{loadError}</Alert>}
153+
154+
{access.isFetching && <Skeleton variant="rounded" height={120} />}
155+
156+
{!access.isFetching && data && (
157+
<>
158+
<Divider />
159+
{data.HasAccess ? (
160+
<Alert severity="warning">
161+
<AlertTitle>
162+
{data.DisplayName} has access via {data.AccessPathCount}{' '}
163+
{data.AccessPathCount === 1 ? 'route' : 'routes'}
164+
</AlertTitle>
165+
Removing one route does not remove the others — every route below has to go for
166+
access to stop.
167+
</Alert>
168+
) : (
169+
<Alert severity="success">
170+
<AlertTitle>{data.DisplayName} has no access</AlertTitle>
171+
{limitedOnly
172+
? 'The only entry found is Limited Access, which SharePoint adds so a user can traverse to a specific item. It does not let them open or list anything here.'
173+
: 'No permission, group membership or sharing link grants this user access to this scope.'}
174+
</Alert>
175+
)}
176+
177+
{data.LibraryInherits && (
178+
<Alert severity="info">
179+
This library inherits its permissions from the site, so the site&apos;s
180+
permissions were evaluated.
181+
</Alert>
182+
)}
183+
184+
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
185+
<Chip size="small" variant="outlined" label={`Scope: ${data.TargetLabel}`} />
186+
{data.IsGuest && (
187+
<Chip size="small" variant="outlined" color="warning" label="Guest account" />
188+
)}
189+
{!data.SharingLinksChecked && (
190+
<Chip
191+
size="small"
192+
variant="outlined"
193+
color="info"
194+
label="Sharing links not checked — no cached data"
195+
/>
196+
)}
197+
</Stack>
198+
199+
<CippDataTable
200+
noCard={true}
201+
isInDialog={true}
202+
title="Access Routes"
203+
queryKey={`SiteUserAccessPaths-${query?.UserPrincipalName}-${query?.ListId || 'root'}`}
204+
data={paths}
205+
simpleColumns={['Route', 'Via', 'PermissionLevel', 'AppliesTo', 'IsSystemManaged']}
206+
/>
207+
</>
208+
)}
209+
</Stack>
210+
</DialogContent>
211+
<DialogActions>
212+
<Button color="inherit" onClick={() => setDrawerVisible(false)}>
213+
Close
214+
</Button>
215+
</DialogActions>
216+
</Dialog>
217+
)
218+
}
219+
220+
export default CippCheckUserAccessDialog

0 commit comments

Comments
 (0)