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