1
0
Fork 0

feat: further work on more narrow permissions

fix-1
Paul Makles 2023-07-30 21:40:19 +01:00
parent 70aa95577a
commit ea39b68703
No known key found for this signature in database
GPG Key ID: 5059F398521BB0F6
7 changed files with 210 additions and 65 deletions

View File

@ -8,13 +8,14 @@ import { RelevantReports } from "@/components/pages/inspector/RelevantReports";
import { UserActions } from "@/components/pages/inspector/UserActions"; import { UserActions } from "@/components/pages/inspector/UserActions";
import { Card, CardHeader } from "@/components/ui/card"; import { Card, CardHeader } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { PLATFORM_MOD_ID } from "@/lib/constants";
import { import {
fetchBotById, fetchBotById,
fetchBotsByUser, fetchBotsByUser,
fetchMembershipsByUser, fetchMembershipsByUser,
fetchMessages, fetchNoticesByUser,
fetchReports, fetchReports,
fetchReportsAgainstUser,
fetchReportsByUser,
fetchServers, fetchServers,
fetchSnapshots, fetchSnapshots,
fetchStrikesByUser, fetchStrikesByUser,
@ -37,17 +38,14 @@ export default async function User({
const bot = user.bot ? await fetchBotById(user._id) : undefined; const bot = user.bot ? await fetchBotById(user._id) : undefined;
const botOwner = user.bot ? await fetchUserById(user.bot.owner) : undefined; const botOwner = user.bot ? await fetchUserById(user.bot.owner) : undefined;
// Fetch strikes // Fetch strikes and moderation alerts
const strikes = await fetchStrikesByUser(user._id); const strikes = await fetchStrikesByUser(user._id);
const notices = await fetchNoticesByUser(user._id);
// Fetch moderation alerts
const dm = await findDM(PLATFORM_MOD_ID, user._id);
const notices = dm ? await fetchMessages({ channel: dm._id }) : [];
// Fetch friends and bots // Fetch friends and bots
const botIds = await fetchBotsByUser(user._id).then((bots) => const botIds = await fetchBotsByUser(user._id)
bots.map((bot) => bot._id) .then((bots) => bots.map((bot) => bot._id))
); .catch(() => []);
const relevantUsers = await fetchUsersById([ const relevantUsers = await fetchUsersById([
...botIds, ...botIds,
@ -59,30 +57,18 @@ export default async function User({
relevantUsers.sort((a) => (a.bot ? -1 : 0)); relevantUsers.sort((a) => (a.bot ? -1 : 0));
// Fetch server memberships // Fetch server memberships
const serverMemberships = await fetchMembershipsByUser(user._id); const serverMemberships = await fetchMembershipsByUser(user._id).catch(
() => []
);
const servers = await fetchServers({ const servers = await fetchServers({
_id: { _id: {
$in: serverMemberships.map((member) => member._id.server), $in: serverMemberships.map((member) => member._id.server),
}, },
}); }).catch(() => []);
// Fetch reports // Fetch reports
const reportsByUser = await fetchReports({ const reportsByUser = await fetchReportsByUser(user._id);
author_id: user._id, const reportsAgainstUser = await fetchReportsAgainstUser(user._id);
});
const reportIdsInvolvingUser = await fetchSnapshots({
// TODO: slow query
$or: [{ "content._id": user._id }, { "content.author": user._id }],
}).then((snapshots) => [
...new Set(snapshots.map((snapshot) => snapshot.report_id)),
]);
const reportsAgainstUser = await fetchReports({
_id: {
$in: reportIdsInvolvingUser,
},
});
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -123,7 +109,7 @@ export default async function User({
<RelevantReports byUser={reportsByUser} forUser={reportsAgainstUser} /> <RelevantReports byUser={reportsByUser} forUser={reportsAgainstUser} />
<Separator /> <Separator />
<RecentMessages query={{ author: user._id }} /> <RecentMessages userId={user._id} />
<Separator /> <Separator />
<JsonCard obj={user} /> <JsonCard obj={user} />

View File

@ -8,7 +8,7 @@ import { NavigationToolbar } from "@/components/common/NavigationToolbar";
import { ReportActions } from "@/components/pages/inspector/ReportActions"; import { ReportActions } from "@/components/pages/inspector/ReportActions";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
fetchRelatedReports, fetchRelatedReportsByContent,
fetchReportById, fetchReportById,
fetchSnapshotsByReport, fetchSnapshotsByReport,
fetchUserById, fetchUserById,
@ -28,7 +28,7 @@ export default async function Reports({ params }: { params: { id: string } }) {
snapshots snapshots
.map((snapshot) => snapshot._id) .map((snapshot) => snapshot._id)
.map((contentId) => .map((contentId) =>
fetchRelatedReports(contentId).then((reports) => fetchRelatedReportsByContent(contentId).then((reports) =>
reports.filter((entry) => entry._id !== report._id) reports.filter((entry) => entry._id !== report._id)
) )
) )

View File

@ -15,7 +15,12 @@ export function ListCompactor<T>({
return ( return (
<> <>
{data.length === 0 && ( {data.length === 0 && (
<h2 className="text-sm text-center pb-2 text-gray-400">Empty List</h2> <>
<h2 className="text-sm text-center pb-2 text-gray-400">Empty List</h2>
<h3 className="text-xs text-center pb-2 text-gray-400">
You may be lacking permissions.
</h3>
</>
)} )}
{data.slice(0, limit).map((item, index) => ( {data.slice(0, limit).map((item, index) => (
<Component key={index} item={item} /> <Component key={index} item={item} />

View File

@ -7,23 +7,32 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../ui/card"; } from "../../ui/card";
import { fetchMessages, fetchUsersById } from "@/lib/db"; import { fetchMessages, fetchMessagesByUser, fetchUsersById } from "@/lib/db";
import { CompactMessage } from "../../cards/CompactMessage"; import { CompactMessage } from "../../cards/CompactMessage";
export async function RecentMessages({ type Props = { users?: boolean | User[] } & (
query, | {
users, query: Filter<Message>;
}: { }
query: Filter<Message>; | {
users?: boolean | User[]; userId: string;
}) { }
const recentMessages = (await fetchMessages(query)).reverse(); );
export async function RecentMessages(props: Props) {
const recentMessages = (
(props as { query: {} }).query
? await fetchMessages((props as { query: Filter<Message> }).query)
: await fetchMessagesByUser((props as { userId: string }).userId).catch(
() => []
)
).reverse();
const userList = ( const userList = (
users === true props.users === true
? await fetchUsersById([...new Set(recentMessages.map((x) => x.author))]) ? await fetchUsersById([...new Set(recentMessages.map((x) => x.author))])
: Array.isArray(users) : Array.isArray(props.users)
? users ? props.users
: [] : []
).reduce((prev, next) => { ).reduce((prev, next) => {
prev[next._id] = next; prev[next._id] = next;
@ -38,6 +47,16 @@ export async function RecentMessages({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{/* enter reason for fetching */} {/* enter reason for fetching */}
{!recentMessages.length && (
<>
<h2 className="text-sm text-center pb-2 text-gray-400">
No messages found
</h2>
<h3 className="text-xs text-center pb-2 text-gray-400">
You may be lacking permissions.
</h3>
</>
)}
{recentMessages.map((message) => ( {recentMessages.map((message) => (
<CompactMessage <CompactMessage
key={message._id} key={message._id}

View File

@ -29,14 +29,8 @@ import {
import dayjs from "dayjs"; import dayjs from "dayjs";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
/** import relativeTime from "dayjs/plugin/relativeTime";
* dayjs.extend(relativeTime);
* @param param0 You have received an account strike, for one or more reasons:
- REASON_HERE
Further violations will result in suspension or a permanent ban depending on severity, please abide by the [Acceptable Usage Policy](https://revolt.chat/aup).
* @returns
*/
export function RelevantModerationNotices({ export function RelevantModerationNotices({
userId, userId,
@ -68,9 +62,7 @@ export function RelevantModerationNotices({
{strikesDraft.map((strike) => ( {strikesDraft.map((strike) => (
<TableRow key={strike._id}> <TableRow key={strike._id}>
<TableCell>{strike.reason}</TableCell> <TableCell>{strike.reason}</TableCell>
<TableCell> <TableCell>{dayjs(decodeTime(strike._id)).fromNow()}</TableCell>
{dayjs(decodeTime(strike._id))?.fromNow()}
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>

View File

@ -13,14 +13,14 @@ type Permission =
| `/fetch${"" | "/by-id" | "/by-user"}` | `/fetch${"" | "/by-id" | "/by-user"}`
| `/update${"" | "/discoverability"}`}` | `/update${"" | "/discoverability"}`}`
| `channels${"" | `/fetch${"" | "/by-id" | "/dm"}` | `/create${"" | "/dm"}`}` | `channels${"" | `/fetch${"" | "/by-id" | "/dm"}` | `/create${"" | "/dm"}`}`
| `messages${"" | `/fetch${"" | "/by-id"}`}` | `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}`
| `reports${ | `reports${
| "" | ""
| `/fetch${ | `/fetch${
| "" | ""
| "/by-id" | "/by-id"
| "/open" | "/open"
| `/related${"" | "/by-content" | "/by-user"}` | `/related${"" | "/by-content" | "/by-user" | "/against-user"}`
| `/snapshots${"" | "/by-report" | "/by-user"}`}` | `/snapshots${"" | "/by-report" | "/by-user"}`}`
| `/update${ | `/update${
| "" | ""
@ -36,24 +36,55 @@ type Permission =
| `/update${"" | "/flags" | "/discoverability"}`}` | `/update${"" | "/flags" | "/discoverability"}`}`
| `users${ | `users${
| "" | ""
| `/fetch${"" | "/by-id" | "/memberships"}` | `/fetch${
| ""
| "/by-id"
| "/memberships"
| "/strikes"
| "/notices"
| "/relations"}`
| `/create${"" | "/alert" | "/strike"}` | `/create${"" | "/alert" | "/strike"}`
| `/update${"" | "/badges"}` | `/update${"" | "/badges"}`
| `/action${"" | "/unsuspend" | "/suspend" | "/wipe" | "/ban"}`}`; | `/action${"" | "/unsuspend" | "/suspend" | "/wipe" | "/ban"}`}`;
const PermissionSets = { const PermissionSets = {
// View open reports
"view-open-reports": [ "view-open-reports": [
// Required for viewing open reports
"users/fetch/by-id", "users/fetch/by-id",
"reports/fetch/open", "reports/fetch/open",
"reports/fetch/by-id", "reports/fetch/by-id",
"reports/fetch/related", "reports/fetch/related",
"reports/fetch/snapshots/by-report", "reports/fetch/snapshots/by-report",
] as Permission[], ] as Permission[],
// Edit reports
"edit-reports": [
"reports/update/notes",
"reports/update/resolve",
"reports/update/reject",
"reports/update/reopen",
] as Permission[],
// Moderate users
"moderate-users": [
"users/fetch/by-id",
"users/fetch/strikes",
"users/fetch/notices",
// "bots/fetch/by-user",
// "messages/fetch/by-user",
// "users/fetch/memberships",
// "servers/fetch",
"reports/fetch/related/by-user",
"reports/fetch/related/against-user",
] as Permission[],
}; };
const Roles = { const Roles = {
moderator: [...PermissionSets["view-open-reports"]], moderator: [
...PermissionSets["view-open-reports"],
...PermissionSets["edit-reports"],
...PermissionSets["moderate-users"],
],
}; };
const ACL: Record<string, Set<Permission>> = { const ACL: Record<string, Set<Permission>> = {

122
lib/db.ts
View File

@ -14,7 +14,8 @@ import type {
} from "revolt-api"; } from "revolt-api";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { publishMessage } from "./redis"; import { publishMessage } from "./redis";
import { checkPermission } from "./accessPermissions"; import { checkPermission, hasPermissionFromSession } from "./accessPermissions";
import { PLATFORM_MOD_ID } from "./constants";
let client: MongoClient; let client: MongoClient;
@ -121,7 +122,16 @@ export async function fetchUserById(id: string) {
return await mongo() return await mongo()
.db("revolt") .db("revolt")
.collection<User>("users") .collection<User>("users")
.findOne({ _id: id }); .findOne(
{ _id: id },
{
projection: {
relations: (await hasPermissionFromSession("users/fetch/relations"))
? 1
: 0,
},
}
);
} }
export async function fetchUsersById(ids: string[]) { export async function fetchUsersById(ids: string[]) {
@ -130,7 +140,16 @@ export async function fetchUsersById(ids: string[]) {
return await mongo() return await mongo()
.db("revolt") .db("revolt")
.collection<User>("users") .collection<User>("users")
.find({ _id: { $in: ids } }) .find(
{ _id: { $in: ids } },
{
projection: {
relations: (await hasPermissionFromSession("users/fetch/relations"))
? 1
: 0,
},
}
)
.toArray(); .toArray();
} }
@ -227,6 +246,26 @@ export async function fetchMessageById(id: string) {
.findOne({ _id: id }); .findOne({ _id: id });
} }
export async function fetchMessagesByUser(userId: string) {
await checkPermission("messages/fetch/by-user");
return await mongo()
.db("revolt")
.collection<Message>("messages")
.find({ author: userId }, { sort: { _id: -1 }, limit: 50 })
.toArray();
}
export async function fetchMessagesByChannel(channelId: string) {
await checkPermission("messages/fetch/by-user");
return await mongo()
.db("revolt")
.collection<Message>("messages")
.find({ channel: channelId }, { sort: { _id: -1 }, limit: 50 })
.toArray();
}
export async function fetchMessages(query: Filter<Message>, limit = 50) { export async function fetchMessages(query: Filter<Message>, limit = 50) {
await checkPermission("messages/fetch"); await checkPermission("messages/fetch");
@ -254,7 +293,7 @@ export async function fetchOpenReports() {
.toArray(); .toArray();
} }
export async function fetchRelatedReports(contentId: string) { export async function fetchRelatedReportsByContent(contentId: string) {
await checkPermission("reports/fetch/related/by-content"); await checkPermission("reports/fetch/related/by-content");
return await mongo() return await mongo()
@ -271,6 +310,57 @@ export async function fetchRelatedReports(contentId: string) {
.toArray(); .toArray();
} }
export async function fetchReportsByUser(userId: string) {
await checkPermission("reports/fetch/related/by-user");
return await mongo()
.db("revolt")
.collection<Report>("safety_reports")
.find(
{ status: "Created", authorId: userId },
{
sort: {
_id: -1,
},
}
)
.toArray();
}
export async function fetchReportsAgainstUser(userId: string) {
await checkPermission("reports/fetch/related/against-user");
const reportIdsInvolvingUser = await mongo()
.db("revolt")
.collection<{ _id: string; report_id: string; content: SnapshotContent }>(
"safety_snapshots"
)
.find({
$or: [{ "content._id": userId }, { "content.author": userId }],
})
.toArray()
.then((snapshots) => [
...new Set(snapshots.map((snapshot) => snapshot.report_id)),
]);
return await mongo()
.db("revolt")
.collection<Report>("safety_reports")
.find(
{
_id: {
$in: reportIdsInvolvingUser,
},
},
{
sort: {
_id: -1,
},
}
)
.toArray();
}
export async function fetchReports( export async function fetchReports(
query: Filter<Report> = { status: "Created" } query: Filter<Report> = { status: "Created" }
) { ) {
@ -332,7 +422,7 @@ export async function fetchSnapshotsByReport(reportId: string) {
} }
export async function fetchStrikesByUser(userId: string) { export async function fetchStrikesByUser(userId: string) {
await checkPermission("reports/fetch/snapshots/by-user"); await checkPermission("users/fetch/strikes");
return await mongo() return await mongo()
.db("revolt") .db("revolt")
@ -348,6 +438,28 @@ export async function fetchStrikesByUser(userId: string) {
.toArray(); .toArray();
} }
export async function fetchNoticesByUser(userId: string) {
await checkPermission("users/fetch/notices");
const dm = await mongo()
.db("revolt")
.collection<Channel & { channel_type: "DirectMessage" }>("channels")
.findOne({
channel_type: "DirectMessage",
recipients: {
$all: [userId, PLATFORM_MOD_ID],
},
});
if (!dm) return [];
return await mongo()
.db("revolt")
.collection<Message>("messages")
.find({ channel: dm!._id }, { sort: { _id: -1 } })
.toArray();
}
export async function fetchBotsByUser(userId: string) { export async function fetchBotsByUser(userId: string) {
await checkPermission("bots/fetch/by-user"); await checkPermission("bots/fetch/by-user");