Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions vault/api/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ type accountRequest struct {
AccessGroupNames []string `json:"access_group_names"`
}

type accountListItem struct {
service.AccountWithSecretCount
CanAccess bool `json:"can_access"`
}

func ListAccounts(c *gin.Context) {
Require(c, RequestTokenExists(c))
accounts, err := service.GetAllAccounts()
Expand All @@ -29,13 +34,14 @@ func ListAccounts(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
authorized := make([]service.AccountWithSecretCount, 0, len(accounts))
response := make([]accountListItem, 0, len(accounts))
for _, account := range accounts {
if RequestTokenCanAccessAccount(c, account.Account) {
authorized = append(authorized, account)
}
response = append(response, accountListItem{
AccountWithSecretCount: account,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact secret metadata for locked accounts

When an authenticated caller lacks this account's access group, the new list item still embeds the full AccountWithSecretCount, so /accounts exposes restricted account metadata such as secret_count, URL, description, group names, and creator/updater IDs even though GetAccount would reject the same caller. If access groups are used to keep an account's secret inventory private, any user can now enumerate that inventory from the list response; return a redacted item for CanAccess == false instead of the full counted model.

Useful? React with 👍 / 👎.

CanAccess: RequestTokenCanAccessAccount(c, account.Account),
})
}
c.JSON(http.StatusOK, authorized)
c.JSON(http.StatusOK, response)
}

func GetAccount(c *gin.Context) {
Expand Down
16 changes: 11 additions & 5 deletions vault/api/app_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,26 @@ type appSecretRequest struct {
Value string `json:"value"`
}

type applicationListItem struct {
service.ApplicationWithSecretCount
CanAccess bool `json:"can_access"`
}

func ListApplications(c *gin.Context) {
Require(c, RequestTokenExists(c))
applications, err := service.GetAllApplications()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
authorized := make([]service.ApplicationWithSecretCount, 0, len(applications))
response := make([]applicationListItem, 0, len(applications))
for _, application := range applications {
if RequestTokenCanAccessApplication(c, application.Application) {
authorized = append(authorized, application)
}
response = append(response, applicationListItem{
ApplicationWithSecretCount: application,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Redact secret counts for locked applications

When a caller is authenticated but not in an application's access group, this still embeds ApplicationWithSecretCount, so /app-secrets reveals the restricted application's secret count and access-group metadata while GetApplication and the secret endpoints remain denied. That lets any user track the contents of applications they cannot open; build a redacted list shape for CanAccess == false or omit the count until the caller is authorized.

Useful? React with 👍 / 👎.

CanAccess: RequestTokenCanAccessApplication(c, application.Application),
})
}
c.JSON(http.StatusOK, authorized)
c.JSON(http.StatusOK, response)
}

func GetApplication(c *gin.Context) {
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,12 @@ export type AppSecretApplicationWithSecrets = AppSecretApplication & {

export type AccountListItem = Account & {
secret_count: number
can_access: boolean
}

export type AppSecretApplicationListItem = AppSecretApplication & {
secret_count: number
can_access: boolean
}

export type AccountInput = {
Expand Down
39 changes: 31 additions & 8 deletions web/src/pages/AccountsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { ArrowUpRight, Clock3, KeyRound, Plus, Search } from "lucide-react"
import { ArrowUpRight, Clock3, KeyRound, Lock, Plus, Search } from "lucide-react"
import { useMemo, useState } from "react"
import { Link } from "react-router-dom"

Expand All @@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
import { listAccounts } from "@/lib/vault"
import { cn } from "@/lib/utils"

function formatDate(value: string) {
return new Date(value).toLocaleDateString(undefined, {
Expand Down Expand Up @@ -86,14 +87,26 @@ export default function AccountsPage() {
</Card>
) : (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{filteredAccounts.map((account) => (
<Link key={account.id} to={`/accounts/${account.id}`} className="group">
<Card className="h-full gap-5 transition-all hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-lg hover:shadow-black/5 dark:hover:border-primary/30 dark:hover:shadow-black/25">
{filteredAccounts.map((account) => {
const card = (
<Card
className={cn(
"h-full gap-5 transition-all",
account.can_access
? "hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-lg hover:shadow-black/5 dark:hover:border-primary/30 dark:hover:shadow-black/25"
: "opacity-60",
)}
>
<CardHeader className="gap-3">
<CardTitle className="flex items-start justify-between gap-3">
<span className="min-w-0 truncate text-base font-semibold">{account.name}</span>
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
<ArrowUpRight className="size-4" />
<span
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground",
account.can_access && "transition-colors group-hover:bg-primary group-hover:text-primary-foreground",
)}
>
{account.can_access ? <ArrowUpRight className="size-4" /> : <Lock className="size-3.5" />}
</span>
</CardTitle>
</CardHeader>
Expand Down Expand Up @@ -122,8 +135,18 @@ export default function AccountsPage() {
</div>
</CardContent>
</Card>
</Link>
))}
)

return account.can_access ? (
<Link key={account.id} to={`/accounts/${account.id}`} className="group">
{card}
</Link>
) : (
<div key={account.id} className="cursor-not-allowed" title="You don't have access to this account">
{card}
</div>
)
})}
</div>
)}
</PageContainer>
Expand Down
39 changes: 31 additions & 8 deletions web/src/pages/AppSecretsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { ArrowUpRight, Clock3, CodeXml, Plus, Search } from "lucide-react"
import { ArrowUpRight, Clock3, CodeXml, Lock, Plus, Search } from "lucide-react"
import { useMemo, useState } from "react"
import { Link } from "react-router-dom"

Expand All @@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
import { listAppSecretApplications, type AppSecretApplicationListItem } from "@/lib/vault"
import { cn } from "@/lib/utils"

function formatDate(value: string) {
return new Date(value).toLocaleDateString(undefined, {
Expand Down Expand Up @@ -91,16 +92,28 @@ export default function AppSecretsPage() {
</Card>
) : (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{filteredApplications.map((application) => (
<Link key={application.id} to={`/app-secrets/${application.id}`} className="group">
<Card className="h-full gap-5 transition-all hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-lg hover:shadow-black/5 dark:hover:border-primary/30 dark:hover:shadow-black/25">
{filteredApplications.map((application) => {
const card = (
<Card
className={cn(
"h-full gap-5 transition-all",
application.can_access
? "hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-lg hover:shadow-black/5 dark:hover:border-primary/30 dark:hover:shadow-black/25"
: "opacity-60",
)}
>
<CardHeader className="gap-3">
<CardTitle className="flex items-start justify-between gap-3">
<span className="min-w-0 truncate font-mono text-base font-semibold">
{application.name}
</span>
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
<ArrowUpRight className="size-4" />
<span
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground",
application.can_access && "transition-colors group-hover:bg-primary group-hover:text-primary-foreground",
)}
>
{application.can_access ? <ArrowUpRight className="size-4" /> : <Lock className="size-3.5" />}
</span>
</CardTitle>
</CardHeader>
Expand Down Expand Up @@ -128,8 +141,18 @@ export default function AppSecretsPage() {
</div>
</CardContent>
</Card>
</Link>
))}
)

return application.can_access ? (
<Link key={application.id} to={`/app-secrets/${application.id}`} className="group">
{card}
</Link>
) : (
<div key={application.id} className="cursor-not-allowed" title="You don't have access to this application">
{card}
</div>
)
})}
</div>
)}
</PageContainer>
Expand Down
Loading