Skip to content

Commit f84d0dc

Browse files
GatewayJovertrueCopilot
authored
Objectcount (#49)
* feat: dataobject count sub account permissions * Update lib/console-policy-parser.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update hooks/use-system.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: resolve lint errors (no-explicit-any, no-unused-vars) --------- Co-authored-by: 安正超 <anzhengchao@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 85d73ad commit f84d0dc

7 files changed

Lines changed: 144 additions & 14 deletions

File tree

‎app/(dashboard)/browser/page.tsx‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { Spinner } from "@/components/ui/spinner"
1616
import { useBucket } from "@/hooks/use-bucket"
1717
import { useObject } from "@/hooks/use-object"
1818
import { useSystem } from "@/hooks/use-system"
19-
import { useAuth } from "@/contexts/auth-context"
2019
import { useDialog } from "@/lib/feedback/dialog"
2120
import { useMessage } from "@/lib/feedback/message"
2221
import { niceBytes } from "@/lib/functions"
@@ -39,7 +38,6 @@ function BrowserBucketsPage() {
3938
const router = useRouter()
4039
const message = useMessage()
4140
const dialog = useDialog()
42-
const { isAdmin } = useAuth()
4341
const { listBuckets, deleteBucket } = useBucket()
4442
const { getDataUsageInfo } = useSystem()
4543

@@ -58,8 +56,14 @@ function BrowserBucketsPage() {
5856
}
5957

6058
try {
61-
const usage = (await getDataUsageInfo()) as { buckets_usage?: BucketUsageMap }
59+
const usage = (await getDataUsageInfo()) as { buckets_usage?: BucketUsageMap } | undefined
6260
if (fetchId !== fetchIdRef.current) return
61+
62+
// getDataUsageInfo returns undefined on 403; don't update data so table shows "--"
63+
if (!usage) {
64+
return
65+
}
66+
6367
const bucketUsage = usage?.buckets_usage ?? {}
6468

6569
setData((prev) =>
@@ -77,6 +81,7 @@ function BrowserBucketsPage() {
7781
)
7882
} catch {
7983
if (fetchId !== fetchIdRef.current) return
84+
// On error, don't update the data - keep showing "--"
8085
} finally {
8186
if (fetchId === fetchIdRef.current) {
8287
setUsageLoading(false)

‎components/object/info.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export function ObjectInfo({
4444
objectKey,
4545
open,
4646
onOpenChange,
47-
onRefresh,
4847
autoPreview = false,
4948
onPreviewChange,
5049
}: ObjectInfoProps) {

‎contexts/api-context.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { AwsClient } from "@/lib/aws4fetch"
77
import { ApiErrorHandler } from "@/lib/api-error-handler"
88
import { useAuth } from "@/contexts/auth-context"
99
import { configManager } from "@/lib/config"
10-
import { getLoginRoute, buildRoute } from "@/lib/routes"
1110

1211
interface ApiContextValue {
1312
api: ApiClient | null

‎contexts/s3-context.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"
55
import { S3Client } from "@aws-sdk/client-s3"
66
import { useAuth } from "@/contexts/auth-context"
77
import { configManager } from "@/lib/config"
8-
import { getLoginRoute } from "@/lib/routes"
98
import type { SiteConfig } from "@/types/config"
109

1110
interface S3Response {

‎hooks/use-system.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ export function useSystem() {
1515
}, [api])
1616

1717
const getDataUsageInfo = useCallback(async () => {
18-
return api.get("/datausageinfo")
18+
try {
19+
return await api.get("/datausageinfo", { suppress403Redirect: true })
20+
} catch (error: unknown) {
21+
const status =
22+
(error as { status?: number })?.status ?? (error as { response?: { status?: number } })?.response?.status
23+
if (status === 403) {
24+
// Preserve previous behavior: treat 403 as "no data" instead of rejecting.
25+
return undefined
26+
}
27+
throw error
28+
}
1929
}, [api])
2030

2131
const getSystemMetrics = useCallback(async () => {

‎lib/api-client.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ interface RequestOptions {
1616
body?: unknown
1717
params?: Record<string, string>
1818
dedupe?: boolean
19+
/**
20+
* If true, 403 errors will throw an error instead of triggering global error handler
21+
* This allows components to handle permission errors gracefully
22+
*/
23+
suppress403Redirect?: boolean
1924
}
2025

2126
const inflightGetRequests = new Map<string, Promise<unknown>>()
@@ -78,6 +83,13 @@ export class ApiClient {
7883
return
7984
}
8085
if (response.status === 403) {
86+
// If suppress403Redirect is true, throw error instead of triggering global handler
87+
// This allows components to handle permission errors gracefully
88+
if (options.suppress403Redirect) {
89+
const errorMsg = await parseApiError(response)
90+
throw new Error(errorMsg)
91+
}
92+
8193
try {
8294
const cloned = response.clone()
8395
let codeText = ""

‎lib/console-policy-parser.ts‎

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { CONSOLE_SCOPES } from "./console-permissions"
33

44
export interface ConsoleStatement {
55
Effect: "Allow" | "Deny"
6-
Action: string[]
6+
Action?: string[]
7+
NotAction?: string[]
78
Resource?: string[]
89
}
910

@@ -12,10 +13,36 @@ export interface ConsolePolicy {
1213
Statement: ConsoleStatement[]
1314
}
1415

15-
function matchAction(policyActions: string[], requestAction: string): boolean {
16+
/**
17+
* Check if an action matches the policy actions
18+
* @param policyActions - Array of action patterns (empty array means match all, undefined means no match)
19+
* @param requestAction - The action to check
20+
* @returns true if the action matches
21+
*/
22+
function matchAction(policyActions: string[] | undefined, requestAction: string): boolean {
23+
// Undefined means no match; explicit empty array means match all actions
24+
if (policyActions === undefined) {
25+
return false
26+
}
27+
if (policyActions.length === 0) {
28+
return true
29+
}
1630
return policyActions.some((pattern) => resourceMatch(pattern, requestAction))
1731
}
1832

33+
/**
34+
* Check if an action matches the NotAction patterns
35+
* @param notActions - Array of action patterns to exclude
36+
* @param requestAction - The action to check
37+
* @returns true if the action should be excluded
38+
*/
39+
function matchNotAction(notActions: string[] | undefined, requestAction: string): boolean {
40+
if (!notActions || notActions.length === 0) {
41+
return false
42+
}
43+
return notActions.some((pattern) => resourceMatch(pattern, requestAction))
44+
}
45+
1946
const IMPLIED_SCOPES: Record<string, string[]> = {
2047
[CONSOLE_SCOPES.VIEW_BROWSER]: [
2148
"s3:ListAllMyBuckets",
@@ -88,6 +115,13 @@ function matchResource(policyResources: string[] | undefined, requestResource: s
88115
return policyResources.some((pattern) => resourceMatch(pattern, requestResource))
89116
}
90117

118+
/**
119+
* Check if an action is a console scope (starts with "console:" or is "consoleAdmin")
120+
*/
121+
function isConsoleScope(action: string): boolean {
122+
return action.startsWith("console:") || action === CONSOLE_SCOPES.CONSOLE_ADMIN
123+
}
124+
91125
export function hasConsolePermission(
92126
policy: ConsolePolicy | ConsoleStatement[] | undefined,
93127
action: string,
@@ -98,28 +132,100 @@ export function hasConsolePermission(
98132
const statements = Array.isArray(policy) ? policy : policy.Statement || []
99133
if (statements.length === 0) return false
100134

101-
const denied = statements.some(
102-
(s) => s.Effect === "Deny" && matchAction(s.Action, action) && matchResource(s.Resource, resource),
103-
)
135+
// For console scopes, we should ignore Resource restrictions if Resource doesn't match "console"
136+
// This allows policies with S3 resources to still grant console permissions
137+
const isConsoleAction = isConsoleScope(action)
138+
const shouldCheckResource = (s: ConsoleStatement): boolean => {
139+
// If action is a console scope and Resource is specified but doesn't match "console",
140+
// we should still allow if Action matches (console scopes are management permissions)
141+
if (isConsoleAction && s.Resource && s.Resource.length > 0) {
142+
// Check if Resource contains console-related resources
143+
const hasConsoleResource = s.Resource.some((r) => r === "console" || r === "*" || r.includes("console"))
144+
// If Resource doesn't contain console resources, skip resource check for console actions
145+
if (!hasConsoleResource) {
146+
return false
147+
}
148+
}
149+
return true
150+
}
151+
152+
// Check Deny statements first
153+
const denied = statements.some((s) => {
154+
if (s.Effect !== "Deny") return false
155+
156+
// If NotAction is present, deny applies to all actions EXCEPT those in NotAction
157+
if (s.NotAction && s.NotAction.length > 0) {
158+
// Deny if action is NOT in NotAction list
159+
if (!matchNotAction(s.NotAction, action)) {
160+
return shouldCheckResource(s) ? matchResource(s.Resource, resource) : false
161+
}
162+
return false
163+
}
164+
165+
// If Action is present (or empty array), deny applies to matching actions
166+
if (matchAction(s.Action, action)) {
167+
return shouldCheckResource(s) ? matchResource(s.Resource, resource) : false
168+
}
169+
170+
return false
171+
})
104172

105173
if (denied) return false
106174

175+
// Check Allow statements
107176
const allowed = statements.some((s) => {
108177
if (s.Effect !== "Allow") return false
109178

179+
// Handle NotAction: allow all actions EXCEPT those in NotAction
180+
if (s.NotAction && s.NotAction.length > 0) {
181+
// If Action is also present, first check if action matches Action
182+
if (s.Action && s.Action.length > 0) {
183+
// Both Action and NotAction present: action must match Action AND not be in NotAction
184+
const actionMatches = matchAction(s.Action, action)
185+
const adminMatch = matchAction(s.Action, CONSOLE_SCOPES.CONSOLE_ADMIN)
186+
const wildcardMatch = matchAction(s.Action, "console:*")
187+
const adminStarMatch = matchAction(s.Action, "admin:*")
188+
189+
if (!(actionMatches || adminMatch || wildcardMatch || adminStarMatch)) {
190+
// Check implied actions
191+
const impliedActions = IMPLIED_SCOPES[action]
192+
if (!impliedActions || !impliedActions.some((implied) => matchAction(s.Action, implied))) {
193+
return false // Action doesn't match Action list
194+
}
195+
}
196+
197+
// Action matches Action list, now check NotAction exclusion
198+
if (matchNotAction(s.NotAction, action)) {
199+
return false // Action is excluded by NotAction
200+
}
201+
} else {
202+
// Only NotAction present: allow all actions except those in NotAction
203+
if (matchNotAction(s.NotAction, action)) {
204+
return false // Action is excluded
205+
}
206+
}
207+
// Action is allowed (matches Action if present, and not in NotAction), check resource
208+
return shouldCheckResource(s) ? matchResource(s.Resource, resource) : true
209+
}
210+
211+
// Only Action is present (or empty array), allow applies to matching actions
110212
const actionMatch = matchAction(s.Action, action)
111213
const adminMatch = matchAction(s.Action, CONSOLE_SCOPES.CONSOLE_ADMIN)
112214
const wildcardMatch = matchAction(s.Action, "console:*")
113215
const adminStarMatch = matchAction(s.Action, "admin:*")
114216

115217
const explicitMatch =
116-
(actionMatch || adminMatch || wildcardMatch || adminStarMatch) && matchResource(s.Resource, resource)
218+
actionMatch || adminMatch || wildcardMatch || adminStarMatch
219+
? shouldCheckResource(s)
220+
? matchResource(s.Resource, resource)
221+
: true
222+
: false
117223
if (explicitMatch) return true
118224

119225
const impliedActions = IMPLIED_SCOPES[action]
120226
if (impliedActions) {
121227
if (impliedActions.some((implied) => matchAction(s.Action, implied))) {
122-
return true
228+
return shouldCheckResource(s) ? matchResource(s.Resource, resource) : true
123229
}
124230
}
125231

0 commit comments

Comments
 (0)