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
|
|
@ -234,6 +234,14 @@ TELESRV_VERIFICATION_ALLOW_USER_TARGETS=false
|
|||
# peer's name) -- a separate mechanism from the checkmark above, not the
|
||||
# platform badge. Its tuning lives in the Advanced section below.
|
||||
TELESRV_BOT_VERIFICATION_ENABLED=true
|
||||
# THIS FEATURE IS NOT FULLY FINISHED AND MAY CAUSE UNSTABLE SERVER BEHAVIOR.
|
||||
# Hides third-party bot verification instead of removing it: the admin panel
|
||||
# drops its "Third-party marks" nav entry and refuses every underlying route
|
||||
# with 404 regardless of session permissions, and the built-in @marksbot
|
||||
# service bot stops responding to messages entirely. Everything stays wired
|
||||
# up (nothing is deleted), so setting this to false re-enables it. Default
|
||||
# true -- leave it alone unless you are specifically testing this feature.
|
||||
TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true
|
||||
# Local composite account-rating score (stars/activity/moderation-based).
|
||||
# Shown to every viewer via userFull.stars_rating, not admin-only. Scoring
|
||||
# weights and recompute timing live in the Advanced section below.
|
||||
|
|
|
|||
|
|
@ -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 配置文件与环境变量,
|
||||
|
|
@ -111,6 +117,7 @@ func loadConfig() (uiConfig, error) {
|
|||
Token: appCfg.AdminUIToken,
|
||||
SessionKey: sum[:],
|
||||
Permissions: appCfg.AdminUIPermissions,
|
||||
HideThirdPartyVerification: appCfg.HideThirdPartyVerification,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) {
|
|||
"actor": "admin",
|
||||
"permissions": permissions.List(),
|
||||
"csrf_token": csrfToken,
|
||||
"hide_third_party_verification": s.cfg.HideThirdPartyVerification,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +280,7 @@ 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(),
|
||||
"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 (
|
||||
<ThirdPartyVerificationHiddenGate>
|
||||
<PermissionGate permission={permissionBotVerificationReview}>
|
||||
<BotVerificationRequestPage id={botVerificationRequestID} navigate={navigate} />
|
||||
</PermissionGate>
|
||||
</ThirdPartyVerificationHiddenGate>
|
||||
);
|
||||
}
|
||||
if (route.path === "/bot-verification") {
|
||||
return (
|
||||
<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 & {
|
||||
|
|
|
|||
|
|
@ -1006,7 +1006,8 @@ func run(logger *zap.Logger) error {
|
|||
botsapp.WithUserStickerSets(accountService),
|
||||
botsapp.WithTelegramLogin(telegramLoginService),
|
||||
botsapp.WithDialogRateLimiter(rateLimiter, cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow),
|
||||
botsapp.WithPublicBaseURL(cfg.PublicBaseURL))
|
||||
botsapp.WithPublicBaseURL(cfg.PublicBaseURL),
|
||||
botsapp.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification))
|
||||
groupCallStore := postgres.NewGroupCallStore(pool)
|
||||
groupCallsService := groupcallsapp.NewService(groupCallStore, groupcallsapp.WithPublicBaseURL(cfg.PublicBaseURL))
|
||||
// 群通话媒体面:内嵌 pion SFU(M1+)。SFU 的 liveness reporter 把媒体面存活
|
||||
|
|
|
|||
|
|
@ -88,8 +88,14 @@ func (s *Service) HandlesBot(botUserID int64) bool {
|
|||
return false
|
||||
}
|
||||
switch botUserID {
|
||||
case domain.VerifierBotUserID:
|
||||
// @marksbot fronts THIRD-PARTY verification, which is hidden by default
|
||||
// (config.HideThirdPartyVerification) because the feature is not fully
|
||||
// finished -- while hidden, the bot doesn't exist as far as the message
|
||||
// pipeline is concerned.
|
||||
return !s.hideThirdPartyVerification
|
||||
case domain.BotFatherUserID, domain.StickersBotUserID, domain.ChatBotUserID,
|
||||
domain.VerifyBotUserID, domain.VerifierBotUserID:
|
||||
domain.VerifyBotUserID:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ type Service struct {
|
|||
now func() time.Time
|
||||
chatBotStreamThrottle time.Duration
|
||||
publicBaseURL string
|
||||
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification: while
|
||||
// true, HandlesBot refuses VerifierBotUserID so @marksbot never answers a
|
||||
// message. The feature is not fully finished and defaults to hidden; see the
|
||||
// config field's doc comment.
|
||||
hideThirdPartyVerification bool
|
||||
// dialogLimiter bounds how often one applicant can drive a service-bot dialog.
|
||||
// The verification service already rate-limits application creation; this is the
|
||||
// separate bound on dialog traffic itself, so a script cannot spin the state
|
||||
|
|
@ -234,6 +239,15 @@ func WithCustomVerification(v customVerifications) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||
// while true, HandlesBot refuses VerifierBotUserID, so @marksbot never
|
||||
// receives or answers a message.
|
||||
func WithHideThirdPartyVerification(hidden bool) Option {
|
||||
return func(s *Service) {
|
||||
s.hideThirdPartyVerification = hidden
|
||||
}
|
||||
}
|
||||
|
||||
// WithVerifierTargets injects the directory of an applicant's own peers used by
|
||||
// @verifierbot's subject picker. It is optional: with nothing injected the bot
|
||||
// falls back to the official verification service's EligibleTargets, which
|
||||
|
|
|
|||
|
|
@ -415,6 +415,28 @@ func TestVerifierBotStartWithoutVerifierStatusIsHonest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestVerifierBotHiddenByThirdPartyVerificationFlag proves
|
||||
// config.HideThirdPartyVerification actually silences @marksbot: with the
|
||||
// option set, HandlesBot must refuse the bot's id entirely, so
|
||||
// OnPrivateMessage never even reaches the dialog logic -- not just an error
|
||||
// reply, no reply at all, matching every other unhandled bot id.
|
||||
func TestVerifierBotHiddenByThirdPartyVerificationFlag(t *testing.T) {
|
||||
cv := newFakeCustomVerification()
|
||||
svc, users, messages := newVerifierBotTestService(t, cv, WithHideThirdPartyVerification(true))
|
||||
owner := newOwner(t, users, "+7201")
|
||||
|
||||
if svc.HandlesBot(domain.VerifierBotUserID) {
|
||||
t.Fatal("service should refuse @verifierbot while third-party verification is hidden")
|
||||
}
|
||||
svc.OnPrivateMessage(context.Background(), domain.VerifierBotUserID, domain.Message{
|
||||
From: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
|
||||
Body: "/start",
|
||||
})
|
||||
if replies := verifierReplies(t, messages, owner.ID); len(replies) != 0 {
|
||||
t.Fatalf("hidden @verifierbot replied: %+v", replies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifierBotStartWithActiveVerifierShowsCompanyAndMark(t *testing.T) {
|
||||
cv := newFakeCustomVerification().activated()
|
||||
svc, users, messages := newVerifierBotTestService(t, cv)
|
||||
|
|
|
|||
|
|
@ -548,6 +548,15 @@ type Config struct {
|
|||
// verifier bots. 0 for either disables the budget.
|
||||
BotVerificationRequestRateLimit int
|
||||
BotVerificationRequestRateWindow time.Duration
|
||||
// HideThirdPartyVerification hides third-party bot verification instead of
|
||||
// removing it: the admin panel drops its "Third-party marks" nav entry and
|
||||
// refuses every botverification.* route with 404 (regardless of session
|
||||
// permissions), and the built-in @marksbot service bot stops responding to
|
||||
// messages entirely. Defaults to true because this feature is NOT FULLY
|
||||
// FINISHED and may cause unstable server behavior -- it stays wired up
|
||||
// (nothing is deleted) so it can be re-enabled later, but it should not be
|
||||
// exposed on a deployment that isn't specifically testing it.
|
||||
HideThirdPartyVerification bool
|
||||
|
||||
// CollectibleUsernameURLTemplate is the landing URL recorded on a minted
|
||||
// collectible username when the mint request carries no explicit URL.
|
||||
|
|
@ -937,6 +946,7 @@ func Load() (Config, error) {
|
|||
// verifier bots, and filing with a second company is not a retry of the first.
|
||||
BotVerificationRequestRateLimit: envIntOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT", 5),
|
||||
BotVerificationRequestRateWindow: envDurationOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", 24*time.Hour),
|
||||
HideThirdPartyVerification: envBoolOr("TELESRV_HIDE_THIRD_PARTY_VERIFICATION", true),
|
||||
|
||||
GroupCallCheckTTL: envDurationOr("TELESRV_GROUPCALL_CHECK_TTL", 45*time.Second),
|
||||
GroupCallSweepInterval: envDurationOr("TELESRV_GROUPCALL_SWEEP_INTERVAL", 10*time.Second),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue