@skyhook-io/radar-app 1.4.1 → 1.4.2
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
-
import { X, Plus, Trash2 } from 'lucide-react'
|
|
2
|
+
import { X, Plus, Trash2, Lock } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import { useAuditSettings, useUpdateAuditSettings, useAudit } from '../../api/client'
|
|
4
|
+
import { useAuditSettings, useUpdateAuditSettings, useAudit, useCloudRole } from '../../api/client'
|
|
5
5
|
import type { CheckMeta } from '@skyhook-io/k8s-ui'
|
|
6
6
|
import { validateRFC1123Label, type ValidationResult } from '@skyhook-io/k8s-ui/utils/validators'
|
|
7
7
|
|
|
@@ -14,6 +14,11 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
14
14
|
const { data: settings } = useAuditSettings()
|
|
15
15
|
const { data: auditData } = useAudit(namespaces)
|
|
16
16
|
const updateSettings = useUpdateAuditSettings()
|
|
17
|
+
// Audit policy is cluster-shared, so writes are owner-gated (enforced
|
|
18
|
+
// server-side too). Non-owners get a read-only view. Non-Cloud callers
|
|
19
|
+
// have no role and pass.
|
|
20
|
+
const { canAtLeast } = useCloudRole()
|
|
21
|
+
const canEdit = canAtLeast('owner')
|
|
17
22
|
const [ignoredNs, setIgnoredNs] = useState<string[]>([])
|
|
18
23
|
const [disabledChecks, setDisabledChecks] = useState<string[]>([])
|
|
19
24
|
const [newNs, setNewNs] = useState('')
|
|
@@ -75,6 +80,15 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
75
80
|
</div>
|
|
76
81
|
|
|
77
82
|
<div className="px-5 py-4 overflow-y-auto flex-1">
|
|
83
|
+
{!canEdit && (
|
|
84
|
+
<div className="mb-4 rounded-lg border border-theme-border bg-theme-elevated/50 p-3 flex items-start gap-2.5">
|
|
85
|
+
<Lock className="w-3.5 h-3.5 mt-0.5 shrink-0 text-theme-text-tertiary" />
|
|
86
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
87
|
+
Audit policy is shared across everyone using this Radar instance, so editing
|
|
88
|
+
is limited to owners. You can review the current settings here.
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
78
92
|
{/* Ignored Namespaces */}
|
|
79
93
|
<div className="mb-6">
|
|
80
94
|
<label className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider">
|
|
@@ -90,7 +104,8 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
90
104
|
<span className="text-sm text-theme-text-primary">{ns}</span>
|
|
91
105
|
<button
|
|
92
106
|
onClick={() => setIgnoredNs(ignoredNs.filter(n => n !== ns))}
|
|
93
|
-
|
|
107
|
+
disabled={!canEdit}
|
|
108
|
+
className="p-1 rounded hover:bg-theme-hover text-theme-text-tertiary hover:text-red-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:text-theme-text-tertiary"
|
|
94
109
|
>
|
|
95
110
|
<Trash2 className="w-3.5 h-3.5" />
|
|
96
111
|
</button>
|
|
@@ -108,6 +123,7 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
108
123
|
onChange={e => setNewNs(e.target.value)}
|
|
109
124
|
onKeyDown={e => { if (e.key === 'Enter') addNamespace() }}
|
|
110
125
|
placeholder="Add namespace..."
|
|
126
|
+
disabled={!canEdit}
|
|
111
127
|
aria-invalid={newNsError ? true : undefined}
|
|
112
128
|
aria-describedby="new-ns-help"
|
|
113
129
|
className={clsx(
|
|
@@ -119,7 +135,7 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
119
135
|
/>
|
|
120
136
|
<button
|
|
121
137
|
onClick={addNamespace}
|
|
122
|
-
disabled={!canAddNamespace}
|
|
138
|
+
disabled={!canEdit || !canAddNamespace}
|
|
123
139
|
className="px-3 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
124
140
|
>
|
|
125
141
|
<Plus className="w-4 h-4" />
|
|
@@ -155,7 +171,8 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
155
171
|
type="checkbox"
|
|
156
172
|
checked={!disabled}
|
|
157
173
|
onChange={() => toggleCheck(check.id)}
|
|
158
|
-
|
|
174
|
+
disabled={!canEdit}
|
|
175
|
+
className="w-4 h-4 rounded border-theme-border text-skyhook-500 focus:ring-skyhook-500 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
159
176
|
/>
|
|
160
177
|
<div className="flex-1 min-w-0">
|
|
161
178
|
<span className="text-sm text-theme-text-primary">{check.title}</span>
|
|
@@ -181,14 +198,16 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
181
198
|
// text — otherwise the user clicks Save expecting their
|
|
182
199
|
// entry to be included and it's silently dropped.
|
|
183
200
|
disabled={
|
|
184
|
-
updateSettings.isPending || newNsError !== null || newNsDuplicate
|
|
201
|
+
!canEdit || updateSettings.isPending || newNsError !== null || newNsDuplicate
|
|
185
202
|
}
|
|
186
203
|
title={
|
|
187
|
-
|
|
188
|
-
? '
|
|
189
|
-
:
|
|
190
|
-
? '
|
|
191
|
-
:
|
|
204
|
+
!canEdit
|
|
205
|
+
? 'Audit settings can only be changed by owners'
|
|
206
|
+
: newNsError
|
|
207
|
+
? 'Fix or clear the pending namespace input before saving'
|
|
208
|
+
: newNsDuplicate
|
|
209
|
+
? 'Clear the duplicate pending input before saving'
|
|
210
|
+
: undefined
|
|
192
211
|
}
|
|
193
212
|
className="px-4 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
194
213
|
>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react'
|
|
2
|
-
import { useAudit, useAuditSettings, useUpdateAuditSettings } from '../../api/client'
|
|
2
|
+
import { useAudit, useAuditSettings, useUpdateAuditSettings, useCloudRole } from '../../api/client'
|
|
3
3
|
import type { SelectedResource } from '../../types'
|
|
4
4
|
import { ChecksView, PaneLoader, type CheckResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
5
|
import { ArrowLeft, ClipboardCheck, Settings } from 'lucide-react'
|
|
@@ -21,6 +21,11 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
21
21
|
const { data, isLoading, error } = useAudit(namespaces)
|
|
22
22
|
const { data: auditSettings } = useAuditSettings()
|
|
23
23
|
const updateSettings = useUpdateAuditSettings()
|
|
24
|
+
// Audit policy is owner-gated (enforced server-side). Withhold the inline
|
|
25
|
+
// hide affordances from non-owners so they don't click into a 403 — the
|
|
26
|
+
// hide menus render only when these callbacks are passed.
|
|
27
|
+
const { canAtLeast } = useCloudRole()
|
|
28
|
+
const canEdit = canAtLeast('owner')
|
|
24
29
|
const [showSettings, setShowSettings] = useState(false)
|
|
25
30
|
|
|
26
31
|
const ignoredCount = auditSettings?.ignoredNamespaces?.length ?? 0
|
|
@@ -105,8 +110,8 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
105
110
|
catalog={data.checks ?? {}}
|
|
106
111
|
anyData
|
|
107
112
|
onResourceClick={onResourceClick}
|
|
108
|
-
onHideCheck={hideCheck}
|
|
109
|
-
onHideCategory={hideCategory}
|
|
113
|
+
onHideCheck={canEdit ? hideCheck : undefined}
|
|
114
|
+
onHideCategory={canEdit ? hideCategory : undefined}
|
|
110
115
|
/>
|
|
111
116
|
|
|
112
117
|
{showSettings && <AuditSettingsDialog namespaces={namespaces} onClose={() => setShowSettings(false)} />}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin, Shield } from 'lucide-react'
|
|
3
|
+
import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin, Shield, Lock } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
6
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
8
|
+
import { useCloudRole } from '../../api/client'
|
|
8
9
|
|
|
9
10
|
interface Config {
|
|
10
11
|
kubeconfig?: string
|
|
@@ -34,6 +35,13 @@ interface SettingsDialogProps {
|
|
|
34
35
|
export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsDialogProps) {
|
|
35
36
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
36
37
|
const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
|
|
38
|
+
// Radar configuration (kubeconfig, port, integrations…) is host-level and
|
|
39
|
+
// affects every user of this instance, so it's gated to owners. Personal
|
|
40
|
+
// sections (My permissions) stay visible to everyone. Non-Cloud callers
|
|
41
|
+
// (OSS, OIDC, kubectl plugin) have no role and pass — single-user laptops
|
|
42
|
+
// are never locked out of their own config. Backend enforces this too.
|
|
43
|
+
const { canAtLeast } = useCloudRole()
|
|
44
|
+
const canEditConfig = canAtLeast('owner')
|
|
37
45
|
const [configData, setConfigData] = useState<ConfigResponse | null>(null)
|
|
38
46
|
const [editedConfig, setEditedConfig] = useState<Config>({})
|
|
39
47
|
const [saving, setSaving] = useState(false)
|
|
@@ -169,33 +177,55 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
169
177
|
</div>
|
|
170
178
|
)}
|
|
171
179
|
{onShowMyPermissions && (
|
|
172
|
-
<div className="mb-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
<div className="mb-5">
|
|
181
|
+
<SectionLabel>Personal</SectionLabel>
|
|
182
|
+
<div className="rounded-md border border-theme-border bg-theme-elevated/50 p-3">
|
|
183
|
+
<div className="flex items-center justify-between gap-3">
|
|
184
|
+
<div className="min-w-0">
|
|
185
|
+
<h3 className="text-sm font-medium text-theme-text-primary">My permissions</h3>
|
|
186
|
+
<p className="mt-0.5 text-xs text-theme-text-tertiary">
|
|
187
|
+
View what your current identity can do in this cluster.
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
<button
|
|
191
|
+
onClick={onShowMyPermissions}
|
|
192
|
+
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover rounded-md transition-colors"
|
|
193
|
+
>
|
|
194
|
+
<Shield className="w-3.5 h-3.5" />
|
|
195
|
+
Open
|
|
196
|
+
</button>
|
|
179
197
|
</div>
|
|
180
|
-
<button
|
|
181
|
-
onClick={onShowMyPermissions}
|
|
182
|
-
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover rounded-md transition-colors"
|
|
183
|
-
>
|
|
184
|
-
<Shield className="w-3.5 h-3.5" />
|
|
185
|
-
Open
|
|
186
|
-
</button>
|
|
187
198
|
</div>
|
|
188
199
|
</div>
|
|
189
200
|
)}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
|
|
202
|
+
<SectionLabel>Radar configuration</SectionLabel>
|
|
203
|
+
{canEditConfig ? (
|
|
204
|
+
<StartupConfigTab
|
|
205
|
+
config={editedConfig}
|
|
206
|
+
effectiveConfig={configData?.effective}
|
|
207
|
+
isDesktop={isDesktop}
|
|
208
|
+
onChange={updateConfigField}
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<div className="rounded-md border border-theme-border bg-theme-elevated/50 p-4 flex items-start gap-3">
|
|
212
|
+
<Lock className="w-4 h-4 mt-0.5 shrink-0 text-theme-text-tertiary" />
|
|
213
|
+
<div className="min-w-0">
|
|
214
|
+
<p className="text-sm font-medium text-theme-text-primary">Owner access required</p>
|
|
215
|
+
<p className="mt-0.5 text-xs text-theme-text-tertiary">
|
|
216
|
+
These settings (kubeconfig, server port, timeline, integrations) affect
|
|
217
|
+
every user of this Radar instance, so they're limited to owners. Ask an
|
|
218
|
+
owner if you need a change here.
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
196
223
|
</div>
|
|
197
224
|
|
|
198
|
-
{/* Footer
|
|
225
|
+
{/* Footer — only the owner-gated config section is editable, so hide
|
|
226
|
+
the save controls entirely for non-owners (personal sections save
|
|
227
|
+
themselves). */}
|
|
228
|
+
{canEditConfig && (
|
|
199
229
|
<div className="flex items-center justify-between gap-3 p-4 border-t border-theme-border shrink-0">
|
|
200
230
|
<div className="flex items-center gap-2">
|
|
201
231
|
<button
|
|
@@ -225,12 +255,23 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
225
255
|
Save
|
|
226
256
|
</button>
|
|
227
257
|
</div>
|
|
258
|
+
)}
|
|
228
259
|
</div>
|
|
229
260
|
</div>,
|
|
230
261
|
document.body
|
|
231
262
|
)
|
|
232
263
|
}
|
|
233
264
|
|
|
265
|
+
// -- Section label ------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
function SectionLabel({ children }: { children: ReactNode }) {
|
|
268
|
+
return (
|
|
269
|
+
<h3 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-2">
|
|
270
|
+
{children}
|
|
271
|
+
</h3>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
234
275
|
// -- Startup Configuration Tab ------------------------------------------------
|
|
235
276
|
|
|
236
277
|
function StartupConfigTab({
|