这是indexloc提供的服务,不要输入任何密码
Skip to content

Implement Role Management and Validation for User Documents #394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ service cloud.firestore {
allow write: if isAtLeastAdmin();
}

match /tanam-users/{uid} {
allow read: if isSignedInAs(uid);
// Delete and create must be done manually
allow update: if isSignedInAs(uid) && request.resource.data.role == resource.data.role;
}

match /tanam-documents/{documentId} {
allow read: if hasAnyRole();
allow write: if isPublisher();
Expand All @@ -24,12 +30,8 @@ service cloud.firestore {
return isSignedIn() && request.auth.token.tanamRole != null;
}

function isSuperAdmin() {
return hasUserRole("superAdmin");
}

function isAtLeastAdmin() {
return isSuperAdmin() || hasUserRole("admin");
return hasUserRole("admin");
}

function isPublisher() {
Expand Down
1 change: 1 addition & 0 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ const app = admin.initializeApp();
app.firestore().settings({ignoreUndefinedProperties: true});

export * from "./genkit";
export * from "./triggers/users";
31 changes: 31 additions & 0 deletions functions/src/models/TanamUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export type TanamRole = "publisher" | "admin";

export interface ITanamUser<TimestampType> {
role?: TanamRole;
createdAt: TimestampType;
updatedAt: TimestampType;
}

export abstract class TanamUser<TimestampType, FieldValueType> {
constructor(id: string, json: ITanamUser<TimestampType>) {
this.id = id;
this.role = json.role ?? "publisher";
this.createdAt = json.createdAt;
this.updatedAt = json.updatedAt;
}

public readonly id: string;
public role: TanamRole;
public readonly createdAt: TimestampType;
public readonly updatedAt: TimestampType;

protected abstract getServerTimestamp(): FieldValueType;

toJson(): object {
return {
role: this.role,
createdAt: this.createdAt ?? this.getServerTimestamp(),
updatedAt: this.getServerTimestamp(),
};
}
}
26 changes: 26 additions & 0 deletions functions/src/models/TanamUserAdmin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {FieldValue, Timestamp} from "firebase-admin/firestore";
import {DocumentSnapshot} from "firebase-functions/v2/firestore";
import {ITanamUser, TanamUser} from "./TanamUser";

export class TanamUserAdmin extends TanamUser<Timestamp, FieldValue> {
constructor(id: string, json: ITanamUser<Timestamp>) {
super(id, json);
}

getServerTimestamp(): FieldValue {
return FieldValue.serverTimestamp();
}

static fromFirestore(snap: DocumentSnapshot): TanamUserAdmin {
const data = snap.data();
if (!data) {
throw new Error("Document data is undefined");
}

return new TanamUserAdmin(snap.id, {
role: data.role,
createdAt: data.createdAt || Timestamp.now(),
updatedAt: data.updatedAt || Timestamp.now(),
});
}
}
85 changes: 85 additions & 0 deletions functions/src/triggers/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as admin from "firebase-admin";
import {Timestamp} from "firebase-admin/firestore";
import {logger} from "firebase-functions/v2";
import {onDocumentCreated, onDocumentDeleted, onDocumentUpdated} from "firebase-functions/v2/firestore";
import {TanamRole} from "../models/TanamUser";
import {TanamUserAdmin} from "../models/TanamUserAdmin";

const auth = admin.auth();
const db = admin.firestore();

// Function to validate and assign role on document creation
// This funciton will scaffold and create a new user document with a role field
// and assert that all the document fields are populated.
export const onTanamUserCreated = onDocumentCreated("tanam-users/{docId}", async (event) => {
const uid = event.params.docId;
const docRef = db.collection("tanam-users").doc(uid);
const docData = (await docRef.get()).data() || {};

logger.info(`Validating User ID: ${uid}`);
try {
await auth.getUser(uid);
} catch (error) {
console.log("Document ID does not match any Firebase Auth UID, deleting document");
return docRef.delete();
}

const existingDocs = await db.collection("tanam-users").get();
const tanamUser = new TanamUserAdmin(uid, {
...docData,
role: existingDocs.size === 1 ? "admin" : "publisher",
createdAt: Timestamp.now(),
updatedAt: Timestamp.now(),
});
logger.info("Creating User", tanamUser.toJson());

const customClaimsBefore = (await auth.getUser(uid)).customClaims || {};
const customClaimsAfter = {...customClaimsBefore, tanamRole: tanamUser.role};

logger.info(`Setting custom claims for ${uid}`, {
customClaimsBefore,
customClaimsAfter,
});

return Promise.all([auth.setCustomUserClaims(uid, customClaimsAfter), docRef.set(tanamUser.toJson())]);
});

// Function to enforce role management on document update
// This function will apply changes to custom claims when the role field is updated
export const onRoleChange = onDocumentUpdated("tanam-users/{docId}", async (event) => {
const uid = event.params.docId;
const beforeData = event?.data?.before.data();
const afterData = event?.data?.after.data();
if (!beforeData || !afterData) {
throw new Error("Document data is undefined");
}

if (beforeData.role === afterData.role) {
logger.debug(`Role unchanged for ${uid}. Stopping here.`);
return;
}

const supportedRoles: TanamRole[] = ["admin", "publisher"];
if (!supportedRoles.includes(afterData.role)) {
logger.error(`Role ${afterData.role} is not supported. Doing nothing.`);
return;
}

logger.info(`Role change detected for ${uid}.`, {before: beforeData.role, after: afterData.role});
return auth.setCustomUserClaims(uid, {tanamRole: afterData.role});
});

// Function to remove role on document deletion
export const onTanamUserDeleted = onDocumentDeleted("tanam-users/{docId}", async (event) => {
const uid = event.params.docId;

console.log(`Document deleted: ${uid}, removing custom claims`);
const customClaims = (await auth.getUser(uid)).customClaims || {};
customClaims.tanamRole = undefined;

logger.info(`Tanam user deleted, removing custom claims for ${uid}`, {
customClaims,
});

await auth.setCustomUserClaims(uid, customClaims);
});