@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- className="p-1 rounded hover:bg-theme-hover text-theme-text-tertiary hover:text-red-400 transition-colors"
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
- className="w-4 h-4 rounded border-theme-border text-skyhook-500 focus:ring-skyhook-500"
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
- newNsError
188
- ? 'Fix or clear the pending namespace input before saving'
189
- : newNsDuplicate
190
- ? 'Clear the duplicate pending input before saving'
191
- : undefined
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-4 rounded-md border border-theme-border bg-theme-elevated/50 p-3">
173
- <div className="flex items-center justify-between gap-3">
174
- <div className="min-w-0">
175
- <h3 className="text-sm font-medium text-theme-text-primary">My permissions</h3>
176
- <p className="mt-0.5 text-xs text-theme-text-tertiary">
177
- View what your current identity can do in this cluster.
178
- </p>
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
- <StartupConfigTab
191
- config={editedConfig}
192
- effectiveConfig={configData?.effective}
193
- isDesktop={isDesktop}
194
- onChange={updateConfigField}
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({