fix: Inconsistent permissions for /oauth2: Access restricted to Global Manager only #196

This commit is contained in:
rustmailer
2026-04-16 23:50:56 +08:00
parent 18a0d52c57
commit d90943bbfe
10 changed files with 45 additions and 75 deletions
+5 -5
View File
@@ -20,8 +20,8 @@ use crate::{
encrypt, id,
modules::{
database::{
delete_impl, insert_impl, manager::DB_MANAGER, paginate_query_primary_scan_all_impl,
async_secondary_find_impl, update_impl,
async_secondary_find_impl, delete_impl, insert_impl, manager::DB_MANAGER,
paginate_query_primary_scan_all_impl, update_impl,
},
error::{code::ErrorCode, BichonResult},
rest::response::DataPage,
@@ -109,9 +109,9 @@ impl OAuth2 {
self.client_id = mask.to_string();
self.client_secret = mask.to_string();
self.auth_url = mask.to_string();
self.token_url = mask.to_string();
self.redirect_uri = mask.to_string();
self.auth_url = self.auth_url.clone();
self.token_url = self.token_url.clone();
self.redirect_uri = self.redirect_uri.clone();
self.scopes = None;
self.extra_params = None;
+9 -30
View File
@@ -35,10 +35,10 @@ pub struct OAuth2Api;
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::OAuth2")]
impl OAuth2Api {
/// Retrieves the OAuth2 configuration for a specified name.
/// Retrieves the OAuth2 configuration for a specified id.
///
/// Requires root privileges.
/// This endpoint fetches the OAuth2 configuration identified by the given name.
/// This endpoint fetches the OAuth2 configuration identified by the given id.
#[oai(
path = "/oauth2/:id",
method = "get",
@@ -46,7 +46,7 @@ impl OAuth2Api {
)]
async fn get_oauth2_config(
&self,
/// The name of the OAuth2 configuration to retrieve
/// The id of the OAuth2 configuration to retrieve
id: Path<u64>,
context: ClientContext,
) -> ApiResult<Json<OAuth2>> {
@@ -57,17 +57,9 @@ impl OAuth2Api {
ErrorCode::ResourceNotFound
)
})?;
if context
.has_permission(None, Permission::ROOT)
.await
{
if context.has_permission(None, Permission::ROOT).await {
return Ok(Json(oauth2));
}
context
.require_permission(None, Permission::ACCOUNT_CREATE)
.await?;
oauth2.scrub_sensitive_fields();
Ok(Json(oauth2))
}
@@ -87,9 +79,7 @@ impl OAuth2Api {
id: Path<u64>,
context: ClientContext,
) -> ApiResult<()> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
Ok(OAuth2::delete(id.0).await?)
}
@@ -108,9 +98,7 @@ impl OAuth2Api {
request: Json<OAuth2CreateRequest>,
context: ClientContext,
) -> ApiResult<()> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
let entity = OAuth2::new(request.0)?;
Ok(entity.save().await?)
}
@@ -132,9 +120,7 @@ impl OAuth2Api {
payload: Json<OAuth2UpdateRequest>,
context: ClientContext,
) -> ApiResult<()> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
Ok(OAuth2::update(id.0, payload.0).await?)
}
@@ -158,17 +144,10 @@ impl OAuth2Api {
context: ClientContext,
) -> ApiResult<Json<DataPage<OAuth2>>> {
let mut list = OAuth2::paginate_list(page.0, page_size.0, desc.0).await?;
if context
.has_permission(None, Permission::ROOT)
.await
{
if context.has_permission(None, Permission::ROOT).await {
return Ok(Json(list));
}
context
.require_permission(None, Permission::ACCOUNT_CREATE)
.await?;
//Non-root users can only view masked data.
for item in &mut list.items {
item.scrub_sensitive_fields();
}
+7 -22
View File
@@ -69,13 +69,8 @@ impl SystemApi {
/// Get the full list of SOCKS5 proxy configurations.
#[oai(method = "get", path = "/list-proxy", operation_id = "list_proxy")]
async fn list_proxy(&self, context: ClientContext) -> ApiResult<Json<Vec<Proxy>>> {
context
.require_any_permission(vec![
(None, Permission::ACCOUNT_CREATE),
(None, Permission::ROOT),
])
.await?;
async fn list_proxy(&self, _context: ClientContext) -> ApiResult<Json<Vec<Proxy>>> {
//The proxy list is visible to all users.
let proxies = Proxy::list_all()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
@@ -90,9 +85,7 @@ impl SystemApi {
id: Path<u64>,
context: ClientContext,
) -> ApiResult<()> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
Ok(Proxy::delete(id.0).await?)
}
@@ -104,18 +97,14 @@ impl SystemApi {
id: Path<u64>,
context: ClientContext,
) -> ApiResult<Json<Proxy>> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
Ok(Json(Proxy::get(id.0).await?))
}
/// Create a new proxy configuration. Requires root permission.
#[oai(path = "/proxy", method = "post", operation_id = "create_proxy")]
async fn create_proxy(&self, url: PlainText<String>, context: ClientContext) -> ApiResult<()> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
let entity = Proxy::new(url.0);
Ok(entity.save().await?)
}
@@ -128,9 +117,7 @@ impl SystemApi {
url: PlainText<String>,
context: ClientContext,
) -> ApiResult<()> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
Ok(Proxy::update(id.0, url.0).await?)
}
/// Get system configurations.
@@ -146,9 +133,7 @@ impl SystemApi {
&self,
context: ClientContext,
) -> ApiResult<Json<SystemConfigurations>> {
context
.require_permission(None, Permission::ROOT)
.await?;
context.require_permission(None, Permission::ROOT).await?;
let config: SystemConfigurations = SystemConfigurations::from(&*SETTINGS);
Ok(Json(config))
}
@@ -65,8 +65,7 @@ export function useSidebarData(): SidebarData {
{
title: t('navigation.oauth2'),
url: '/oauth2',
icon: IdCard,
visible: require_any_permission(['system:root', 'account:create']),
icon: IdCard
}
]
},
@@ -33,9 +33,9 @@ export const getColumns = (t: (key: string) => string): ColumnDef<OAuth2Entity>[
<DataTableColumnHeader column={column} title={t('settings.id')} />
),
cell: ({ row }) => {
return <LongText className='max-w-[100px]'>{row.original.id}</LongText>
return <LongText className='max-w-[140px]'>{row.original.id}</LongText>
},
meta: { className: 'max-w-[100px]' },
meta: { className: 'max-w-[140px]' },
enableHiding: false,
enableSorting: false,
},
@@ -92,9 +92,9 @@ export const getColumns = (t: (key: string) => string): ColumnDef<OAuth2Entity>[
<DataTableColumnHeader column={column} title={t('settings.description')} />
),
cell: ({ row }) => (
<LongText className='max-w-[200px]'>{row.original.description}</LongText>
<LongText className='max-w-[180px]'>{row.original.description}</LongText>
),
meta: { className: 'max-w-[200px]' },
meta: { className: 'max-w-[180px]' },
enableHiding: true,
enableSorting: false
},
@@ -33,6 +33,7 @@ import { useOAuth2Context } from '../context'
import { OAuth2Entity } from '../data/schema'
import { WorkflowIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
interface DataTableRowActionsProps {
@@ -42,6 +43,9 @@ interface DataTableRowActionsProps {
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { t } = useTranslation()
const { setOpen, setCurrentRow } = useOAuth2Context()
const { require_any_permission } = useCurrentUser()
return (
<>
<DropdownMenu modal={false}>
@@ -56,6 +60,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[160px]'>
<DropdownMenuItem
disabled={!require_any_permission(['system:root'])}
onClick={() => {
setCurrentRow(row.original)
setOpen('edit')
@@ -80,6 +85,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={!require_any_permission(['system:root'])}
onClick={() => {
setCurrentRow(row.original)
setOpen('delete')
+5 -8
View File
@@ -37,12 +37,13 @@ import { TableSkeleton } from '@/components/table-skeleton'
import { AuthorizeDialog } from './components/authorize-dialog'
import { FixedHeader } from '@/components/layout/fixed-header'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
export default function OAuth2() {
const { t } = useTranslation()
const [currentRow, setCurrentRow] = useState<OAuth2Entity | null>(null)
const [open, setOpen] = useDialogState<OAuth2DialogType>(null)
const { require_any_permission } = useCurrentUser()
const { data: oauth2List, isLoading } = useQuery({
queryKey: ['oauth2-list'],
@@ -53,12 +54,9 @@ export default function OAuth2() {
return (
<OAuth2Provider value={{ open, setOpen, currentRow, setCurrentRow }}>
{/* ===== Top Heading ===== */}
<FixedHeader />
<Main>
<div className="mx-auto w-full max-w-6xl px-4">
{/* Header */}
<div className="mx-auto max-w-[88rem] px-4">
<div className="mb-2 flex items-start flex-wrap gap-x-4 gap-y-2">
<div className="flex-1 min-w-[300px]">
<h2 className="text-2xl font-bold tracking-tight">{t('oauth2.title')}</h2>
@@ -67,13 +65,12 @@ export default function OAuth2() {
</p>
</div>
<div className="flex gap-2 ml-auto">
<Button className="space-x-1" onClick={() => setOpen("add")}>
<Button className="space-x-1" disabled={!require_any_permission(['system:root'])} onClick={() => setOpen("add")}>
<span>{t('common.add')}</span>
<Plus size={18} />
</Button>
</div>
</div>
{/* Table / Empty State */}
<div className="flex-1 overflow-auto py-1 flex-row lg:space-x-12 space-y-0">
{isLoading ? (
<TableSkeleton columns={columns.length} rows={10} />
@@ -91,7 +88,7 @@ export default function OAuth2() {
<p className="mb-4 mt-2 text-sm text-muted-foreground">
{t('oauth2.noConfigurationsDesc')}
</p>
<Button onClick={() => setOpen("add")}>{t('oauth2.addConfiguration')}</Button>
<Button disabled={!require_any_permission(['system:root'])} onClick={() => setOpen("add")}>{t('oauth2.addConfiguration')}</Button>
</div>
</div>
)}
+1 -2
View File
@@ -27,7 +27,7 @@ import { useTranslation } from 'react-i18next'
export default function Settings() {
const { t } = useTranslation()
const { require_any_permission, canGlobal } = useCurrentUser()
const { canGlobal } = useCurrentUser()
const sidebarNavItems = [
@@ -50,7 +50,6 @@ export default function Settings() {
title: t('settings.sidebar.proxy'),
icon: <Waypoints size={18} />,
href: '/settings/proxy',
visible: require_any_permission(['system:root', 'account:create']),
},
{
title: t('settings.sidebar.configurations'),
@@ -32,6 +32,7 @@ import {
import { useProxyContext } from '../context'
import { useTranslation } from 'react-i18next'
import { Proxy } from '@/api/system/api'
import { useCurrentUser } from '@/hooks/use-current-user'
interface DataTableRowActionsProps {
@@ -40,6 +41,7 @@ interface DataTableRowActionsProps {
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { setOpen, setCurrentRow } = useProxyContext()
const { require_any_permission } = useCurrentUser()
const { t } = useTranslation()
return (
<>
@@ -55,6 +57,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[160px]'>
<DropdownMenuItem
disabled={!require_any_permission(['system:root'])}
onClick={() => {
setCurrentRow(row.original)
setOpen('edit')
@@ -67,6 +70,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={!require_any_permission(['system:root'])}
onClick={() => {
setCurrentRow(row.original)
setOpen('delete')
+3 -2
View File
@@ -33,13 +33,14 @@ import Logo from '@/assets/logo.svg'
import useProxyList from '@/hooks/use-proxy'
import { useTranslation } from 'react-i18next'
import { Proxy } from '@/api/system/api'
import { useCurrentUser } from '@/hooks/use-current-user'
export default function ProxyManagerPage() {
const { t } = useTranslation()
const [currentRow, setCurrentRow] = useState<Proxy | null>(null)
const [open, setOpen] = useDialogState<ProxyDialogType>(null)
const { require_any_permission } = useCurrentUser()
const { proxyList, isLoading } = useProxyList()
const columns = getColumns(t)
@@ -49,7 +50,7 @@ export default function ProxyManagerPage() {
<div>
<div className="mb-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
<div className="flex gap-2">
<Button className="space-x-1" onClick={() => setOpen('add')}>
<Button className="space-x-1" disabled={!require_any_permission(['system:root'])} onClick={() => setOpen('add')}>
<span>{t('settings.add')}</span> <Plus size={18} />
</Button>
</div>