mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-20 06:37:23 +02:00
fix: rearm owner alerts across recovery actions safely
This commit is contained in:
1 parent
d519a0a9bb
commit
633e5ddcae
9 files changed
+117
-10
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { Schema } from "mongoose";
|
||||
|
||||
const AnonymizedGistSchema = new Schema({
|
||||
accessAlertGeneration: { type: String, select: false },
|
||||
accessAlertClaimedAt: { type: Date, select: false },
|
||||
gistId: {
|
||||
type: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { repositoryAccessSchema } from "../repository-access.schema";
|
||||
import { Schema } from "mongoose";
|
||||
|
||||
const AnonymizedPullRequestSchema = new Schema({
|
||||
accessAlertGeneration: { type: String, select: false },
|
||||
accessAlertClaimedAt: { type: Date, select: false },
|
||||
pullRequestId: {
|
||||
type: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { repositoryAccessSchema } from "../repository-access.schema";
|
||||
import { Schema } from "mongoose";
|
||||
|
||||
const AnonymizedRepositorySchema = new Schema({
|
||||
accessAlertGeneration: { type: String, select: false },
|
||||
accessAlertClaimedAt: { type: Date, select: false },
|
||||
reviewConsentRevision: { type: Number, select: false },
|
||||
repoId: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { Types } from "mongoose";
|
||||
import { isGitHubRateLimitError } from "./github-rate-limit";
|
||||
import config from "../config";
|
||||
@@ -19,7 +20,7 @@ export async function resetOwnerAccessAlerts(ownerId: string, resource?: AlertRe
|
||||
const filter = { owner: new Types.ObjectId(ownerId),
|
||||
...(resource ? { _id: new Types.ObjectId(resource.id) } : {}) };
|
||||
for (const collection of resource ? [resourceCollections[resource.kind]] : Object.values(resourceCollections)) {
|
||||
await UserModel.db.collection(collection).updateMany(filter, { $unset: { accessAlertClaimedAt: "" } });
|
||||
await UserModel.db.collection(collection).updateMany(filter, { $unset: { accessAlertClaimedAt: "" }, $set: { accessAlertGeneration: randomUUID() } });
|
||||
}
|
||||
} catch { logger.warn("owner access alert reset unavailable"); }
|
||||
}
|
||||
@@ -66,14 +67,20 @@ export async function notifyOwnerAccessProblem(ownerId: string, error: unknown,
|
||||
if (!config.RESEND_API_KEY || !config.EMAIL_FROM || !isAccessFailure(error)) return;
|
||||
try {
|
||||
if (!resource || !Types.ObjectId.isValid(resource.id) || !Types.ObjectId.isValid(ownerId)) return;
|
||||
const collection = UserModel.db.collection(resourceCollections[resource.kind]);
|
||||
const identity = { _id: new Types.ObjectId(resource.id), owner: new Types.ObjectId(ownerId) };
|
||||
const snapshot = await collection.findOne(identity, { projection: { accessAlertGeneration: 1 } });
|
||||
if (!snapshot) return;
|
||||
const owner = await UserModel.findOne({ _id: ownerId, status: { $nin: ["removed", "banned"] } }).lean();
|
||||
if (!owner) return;
|
||||
const to = notificationEmail(owner.emails, owner.notificationEmail);
|
||||
if (!to) return;
|
||||
// Claim the resource atomically before delivery. This has no expiry: neither
|
||||
// repeated readers nor workers can send again until the owner acts.
|
||||
const claim = await UserModel.db.collection(resourceCollections[resource.kind]).updateOne({
|
||||
_id: new Types.ObjectId(resource.id), owner: new Types.ObjectId(ownerId),
|
||||
const claim = await collection.updateOne({
|
||||
...identity,
|
||||
// A reset during the owner lookup invalidates this older failure.
|
||||
accessAlertGeneration: snapshot.accessAlertGeneration ?? { $exists: false },
|
||||
accessAlertClaimedAt: { $exists: false },
|
||||
}, { $set: { accessAlertClaimedAt: new Date() } });
|
||||
if (!claim.modifiedCount) return;
|
||||
|
||||
@@ -149,6 +149,7 @@ router.get("/app/setup", enabled, async (req, res) => {
|
||||
if (req.query.setup_action !== "request") {
|
||||
const installations = await userInstallations(user.id);
|
||||
if (!installations.some(i => String(i.id) === req.query.installation_id)) throw appError("github_app_access_required");
|
||||
await resetOwnerAccessAlerts(user.id);
|
||||
}
|
||||
await UserModel.updateOne({ _id: user.id }, { $set: { repositories: [] } });
|
||||
res.redirect(flow.returnTo);
|
||||
|
||||
@@ -115,7 +115,8 @@ router.post("/claim", async (req, res) => {
|
||||
|
||||
await AnonymizedRepositoryModel.updateOne(
|
||||
{ repoId: repoConfig.repoId },
|
||||
{ $set: { owner: user.model.id, githubAccess: selectedAccess.binding } }
|
||||
{ $set: { owner: user.model.id, githubAccess: selectedAccess.binding, accessAlertGeneration: randomUUID() },
|
||||
$unset: { accessAlertClaimedAt: "" } }
|
||||
).collation({ locale: "en", strength: 2 });
|
||||
return res.send("Ok");
|
||||
} catch (error) {
|
||||
@@ -199,6 +200,7 @@ router.post(
|
||||
{ $set: updates }
|
||||
).exec();
|
||||
if (saved && saved.matchedCount === 0) throw new AnonymousError("invalid_status", { httpStatus: 409 });
|
||||
if (user.id === repo.owner.id) await resetOwnerAccessAlerts(user.id, { kind: "repository", id: String(repo.model._id) });
|
||||
|
||||
if (reactivating) {
|
||||
// Expiration removes the cached files. Rebuild the saved commit
|
||||
|
||||
Reference in new issue
Block a user