added ability to disable third-party verification
This commit is contained in:
parent
4f0fa895c1
commit
36350f83dc
17 changed files with 211 additions and 69 deletions
|
|
@ -27,9 +27,24 @@ import (
|
|||
// journal, the status machine and the optimistic lock are enforced in one place and
|
||||
// a panel action is indistinguishable from an API one in the audit trail.
|
||||
|
||||
// requireThirdPartyVerificationVisible refuses every third-party verification
|
||||
// route while the feature is hidden (TELESRV_HIDE_THIRD_PARTY_VERIFICATION,
|
||||
// default true), regardless of session permissions -- the feature is not
|
||||
// fully finished and may cause unstable server behavior, so hiding it is
|
||||
// enforced here, not just by the panel dropping its nav entry.
|
||||
func (s *server) requireThirdPartyVerificationVisible(handler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.cfg.HideThirdPartyVerification {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// botVerificationRead mounts a route behind a session and botverification.review.
|
||||
func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler {
|
||||
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, handler))
|
||||
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, s.requireThirdPartyVerificationVisible(handler)))
|
||||
}
|
||||
|
||||
// botVerificationManage mounts a route behind a session and botverification.manage.
|
||||
|
|
@ -38,7 +53,7 @@ func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler {
|
|||
// a verifier and working its queue are different jobs, so an operator may hold
|
||||
// either without the other.
|
||||
func (s *server) botVerificationManage(handler http.HandlerFunc) http.Handler {
|
||||
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, handler))
|
||||
return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, s.requireThirdPartyVerificationVisible(handler)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -76,6 +76,12 @@ type uiConfig struct {
|
|||
// entry, so introducing the permission model never locks an operator out of a
|
||||
// panel that worked before.
|
||||
Permissions []string
|
||||
// HideThirdPartyVerification mirrors config.HideThirdPartyVerification
|
||||
// (TELESRV_HIDE_THIRD_PARTY_VERIFICATION, default true): while true, every
|
||||
// botverification.* route refuses with 404 regardless of session
|
||||
// permissions, and the session/login response tells the frontend to hide
|
||||
// the "Third-party marks" nav entry and its routes.
|
||||
HideThirdPartyVerification bool
|
||||
}
|
||||
|
||||
// loadConfig 通过 internal/config.Load() 加载 .env 配置文件与环境变量,
|
||||
|
|
@ -103,14 +109,15 @@ func loadConfig() (uiConfig, error) {
|
|||
sum := sha256.Sum256([]byte(appCfg.AdminSessionKey))
|
||||
|
||||
return uiConfig{
|
||||
Addr: appCfg.AdminUIAddr,
|
||||
PostgresDSN: appCfg.PostgresDSN,
|
||||
AdminAPIURL: adminAPIURL(adminAPIAddr),
|
||||
AdminAPIToken: appCfg.AdminAPIToken,
|
||||
Password: appCfg.AdminUIPassword,
|
||||
Token: appCfg.AdminUIToken,
|
||||
SessionKey: sum[:],
|
||||
Permissions: appCfg.AdminUIPermissions,
|
||||
Addr: appCfg.AdminUIAddr,
|
||||
PostgresDSN: appCfg.PostgresDSN,
|
||||
AdminAPIURL: adminAPIURL(adminAPIAddr),
|
||||
AdminAPIToken: appCfg.AdminAPIToken,
|
||||
Password: appCfg.AdminUIPassword,
|
||||
Token: appCfg.AdminUIToken,
|
||||
SessionKey: sum[:],
|
||||
Permissions: appCfg.AdminUIPermissions,
|
||||
HideThirdPartyVerification: appCfg.HideThirdPartyVerification,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -251,9 +251,10 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
setCSRFCookie(w, csrfToken, sessionTTL)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"actor": "admin",
|
||||
"permissions": permissions.List(),
|
||||
"csrf_token": csrfToken,
|
||||
"actor": "admin",
|
||||
"permissions": permissions.List(),
|
||||
"csrf_token": csrfToken,
|
||||
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -277,8 +278,9 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) {
|
|||
// than letting them walk into a 403.
|
||||
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"actor": actorFromContext(r.Context()),
|
||||
"permissions": permissionsFromContext(r.Context()).List(),
|
||||
"actor": actorFromContext(r.Context()),
|
||||
"permissions": permissionsFromContext(r.Context()).List(),
|
||||
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
10
cmd/telesrv-admin/web/dist/assets/index-BUraMCki.js
vendored
Normal file
10
cmd/telesrv-admin/web/dist/assets/index-BUraMCki.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
cmd/telesrv-admin/web/dist/index.html
vendored
2
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -23,7 +23,7 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-CP1sn_7z.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BUraMCki.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-XySYVUb7.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function App() {
|
|||
}
|
||||
|
||||
return (
|
||||
<PermissionsProvider permissions={session.permissions ?? []}>
|
||||
<PermissionsProvider permissions={session.permissions ?? []} hideThirdPartyVerification={session.hide_third_party_verification ?? true}>
|
||||
<Shell actor={session.actor} route={route} navigate={navigate} onLogout={() => setSession(null)}>
|
||||
<Routes route={route} navigate={navigate} />
|
||||
</Shell>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { api } from "../api";
|
||||
import { permissionBotVerificationReview, permissionVerificationReview, useCan } from "../permissions";
|
||||
import { permissionBotVerificationReview, permissionVerificationReview, useCan, useThirdPartyVerificationHidden } from "../permissions";
|
||||
import { type Navigate, type RouteState, routeTitle } from "../routing";
|
||||
import { ThemeSwitch } from "../theme";
|
||||
import { AppLink } from "./AppLink";
|
||||
|
|
@ -58,6 +58,9 @@ export function Shell({
|
|||
// Same reasoning for the third-party queue, which has its own right: the two
|
||||
// sections are granted independently, so one entry can be visible without the other.
|
||||
const canReviewBotVerification = useCan(permissionBotVerificationReview);
|
||||
// Third-party verification is additionally hidden by default (not fully
|
||||
// finished) regardless of what the session was granted -- see permissions.tsx.
|
||||
const thirdPartyVerificationHidden = useThirdPartyVerificationHidden();
|
||||
const messagesActive = route.path.startsWith("/messages");
|
||||
const [messagesOpen, setMessagesOpen] = useState(messagesActive);
|
||||
|
||||
|
|
@ -92,7 +95,7 @@ export function Shell({
|
|||
{canReviewVerification && (
|
||||
<NavLink icon={<BadgeCheck size={16} />} href="/verification" route={route} navigate={navigate}>{"Verification"}</NavLink>
|
||||
)}
|
||||
{canReviewBotVerification && (
|
||||
{canReviewBotVerification && !thirdPartyVerificationHidden && (
|
||||
<NavLink icon={<Stamp size={16} />} href="/bot-verification" route={route} navigate={navigate}>{"Third-party marks"}</NavLink>
|
||||
)}
|
||||
<NavLink icon={<AtSign size={16} />} href="/collectible-usernames" route={route} navigate={navigate}>{"NFT Usernames"}</NavLink>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { VerificationDetailPage } from "./VerificationDetailPage";
|
|||
import { VerificationPage } from "./VerificationPage";
|
||||
import {
|
||||
PermissionGate,
|
||||
ThirdPartyVerificationHiddenGate,
|
||||
permissionBotVerificationReview,
|
||||
permissionVerificationReview
|
||||
} from "../permissions";
|
||||
|
|
@ -45,16 +46,20 @@ export function Routes({ route, navigate }: { route: RouteState; navigate: Navig
|
|||
const botVerificationRequestID = route.path.match(/^\/bot-verification\/(\d+)$/)?.[1];
|
||||
if (botVerificationRequestID) {
|
||||
return (
|
||||
<PermissionGate permission={permissionBotVerificationReview}>
|
||||
<BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} />
|
||||
</PermissionGate>
|
||||
<ThirdPartyVerificationHiddenGate>
|
||||
<PermissionGate permission={permissionBotVerificationReview}>
|
||||
<BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} />
|
||||
</PermissionGate>
|
||||
</ThirdPartyVerificationHiddenGate>
|
||||
);
|
||||
}
|
||||
if (route.path === "/bot-verification") {
|
||||
return (
|
||||
<PermissionGate permission={permissionBotVerificationReview}>
|
||||
<BotVerificationPage navigate={navigate} />
|
||||
</PermissionGate>
|
||||
<ThirdPartyVerificationHiddenGate>
|
||||
<PermissionGate permission={permissionBotVerificationReview}>
|
||||
<BotVerificationPage navigate={navigate} />
|
||||
</PermissionGate>
|
||||
</ThirdPartyVerificationHiddenGate>
|
||||
);
|
||||
}
|
||||
// The detail match has to be tested before the exact "/verification" branch, and
|
||||
|
|
|
|||
|
|
@ -17,20 +17,32 @@ export const permissionBotVerificationManage = "botverification.manage";
|
|||
// section the session may not use is hidden instead of rendered into a 403. This
|
||||
// is a convenience for the operator, not a security boundary: every route is
|
||||
// checked again server-side.
|
||||
const PermissionsContext = createContext<readonly string[]>([]);
|
||||
type SessionFlags = {
|
||||
permissions: readonly string[];
|
||||
// Mirrors AdminSession.hide_third_party_verification. Deliberately NOT folded
|
||||
// into the permission list: it applies regardless of what the session was
|
||||
// granted (even "*"), because the feature is not fully finished rather than
|
||||
// merely restricted.
|
||||
hideThirdPartyVerification: boolean;
|
||||
};
|
||||
|
||||
const PermissionsContext = createContext<SessionFlags>({ permissions: [], hideThirdPartyVerification: true });
|
||||
|
||||
export function PermissionsProvider({
|
||||
permissions,
|
||||
hideThirdPartyVerification = true,
|
||||
children
|
||||
}: {
|
||||
permissions: readonly string[];
|
||||
hideThirdPartyVerification?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <PermissionsContext.Provider value={permissions}>{children}</PermissionsContext.Provider>;
|
||||
const value = useMemo(() => ({ permissions, hideThirdPartyVerification }), [permissions, hideThirdPartyVerification]);
|
||||
return <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>;
|
||||
}
|
||||
|
||||
export function usePermissions(): { permissions: readonly string[]; can: (permission: string) => boolean } {
|
||||
const permissions = useContext(PermissionsContext);
|
||||
const { permissions } = useContext(PermissionsContext);
|
||||
return useMemo(
|
||||
() => ({
|
||||
permissions,
|
||||
|
|
@ -44,6 +56,13 @@ export function useCan(permission: string): boolean {
|
|||
return usePermissions().can(permission);
|
||||
}
|
||||
|
||||
// useThirdPartyVerificationHidden reports the server's
|
||||
// TELESRV_HIDE_THIRD_PARTY_VERIFICATION setting (default true). Unlike
|
||||
// useCan, this is never overridden by a "*" session -- see SessionFlags.
|
||||
export function useThirdPartyVerificationHidden(): boolean {
|
||||
return useContext(PermissionsContext).hideThirdPartyVerification;
|
||||
}
|
||||
|
||||
// PermissionGate is what a direct URL hits: without the right the operator gets
|
||||
// an explanation naming the missing permission, not an empty table that looks
|
||||
// like "no data".
|
||||
|
|
@ -70,3 +89,26 @@ export function PermissionDenied({ permission }: { permission: string }) {
|
|||
</PageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
// ThirdPartyVerificationHiddenGate is what a direct URL to a third-party
|
||||
// verification page hits while the feature is hidden -- distinct from
|
||||
// PermissionGate because no permission grant (not even "*") changes this.
|
||||
export function ThirdPartyVerificationHiddenGate({ children }: { children: ReactNode }) {
|
||||
const hidden = useThirdPartyVerificationHidden();
|
||||
if (!hidden) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return (
|
||||
<PageFrame title={"Feature hidden"} eyebrow={"Console / Third-party marks"}>
|
||||
<Alert>{"Third-party bot verification is hidden on this server (TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true)."}</Alert>
|
||||
<section className="section-block">
|
||||
<div className="entity-head">
|
||||
<div>
|
||||
<div className="entity-title"><ShieldOff size={16} /> {"Not fully finished"}</div>
|
||||
<div className="entity-subtitle">{"This feature may cause unstable server behavior and is hidden by default. Set TELESRV_HIDE_THIRD_PARTY_VERIFICATION=false to re-enable it."}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageFrame>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -718,6 +718,13 @@ export type AdminSession = {
|
|||
actor: string;
|
||||
// The right set the signed session was issued with; ["*"] means everything.
|
||||
permissions?: string[] | null;
|
||||
// Mirrors the server's TELESRV_HIDE_THIRD_PARTY_VERIFICATION (default true):
|
||||
// while true, the panel drops the "Third-party marks" nav entry and its
|
||||
// routes, regardless of what permissions the session carries -- the feature
|
||||
// is not fully finished. The server also refuses the underlying routes with
|
||||
// 404, so this is a UI convenience on top of a real enforcement, not the
|
||||
// enforcement itself.
|
||||
hide_third_party_verification?: boolean;
|
||||
};
|
||||
|
||||
export type AdminLoginResult = AdminSession & {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue