我最近解决了这个用例,将自定义声明作为“角色”数组字段复制到相应的 firestore 'users/{uid}/private-user/{data}' 文档中。在我的场景中,我必须区分两个角色(“admin”和“superadmin”)。 firestore 'users/' 集合的文档是公开的,而 'users/{uid}/private-user/' 集合的文档只能从拥有用户和“超级管理员”用户的客户端,或通过 Firestore Admin SDK(服务器端)也只能作为“超级管理员”用户。
此外,我只想允许“超级管理员”用户添加或删除“超级管理员”或“管理员”角色/声明;或获取“超级管理员”或“管理员”用户列表。
数据重复在 NoSQL 世界中非常普遍,并不被视为一种不好的做法。
这是我的代码 (Node.js/TypeScript)
首先,firebase 云功能实现(需要 Admin SDK)添加自定义用户声明/角色。
注意,“超级管理员”验证行
await validateUserClaim(functionName, context, "superadmin")
在创建至少一个“超级管理员”之前必须删除,以后可以使用该“超级管理员”向用户添加或删除其他角色/声明!
const functionName = "add-admin-user"
export default async (
payload: string,
context: CallableContext,
): Promise<void> => {
try {
validateAuthentication(functionName, context)
validateEmailVerified(functionName, context)
await validateUserClaim(functionName, context, "superadmin")
const request = parseRequestPayload<AddAdminUserRoleRequest>(
functionName,
payload,
)
// Note, to remove a custom claim just use "{ [request.roleName]: null }"
// as second input parameter.
await admin
.auth()
.setCustomUserClaims(request.uid, { [request.roleName]: true })
const userDoc = await db
.collection(`users/${request.uid}/private-user`)
.doc("data")
.get()
const roles = userDoc.data()?.roles ?? []
if (roles.indexOf(request.roleName) === -1) {
roles.push(request.roleName)
db.collection(`users/${request.uid}/private-user`)
.doc("data")
.set({ roles }, { merge: true })
}
} catch (e) {
throw logAndReturnHttpsError(
"internal",
`Firestore ${functionName} not executed. Failed to add 'admin' or ` +
`'superadmin' claim to user. (${(<Error>e)?.message})`,
`${functionName}/internal`,
e,
)
}
}
其次,返回“超级管理员”或“管理员”用户列表的firebase云功能实现(需要Admin SDK)。
const functionName = "get-admin-users"
export default async (
payload: string,
context: CallableContext,
): Promise<GetAdminUsersResponse> => {
try {
validateAuthentication(functionName, context)
validateEmailVerified(functionName, context)
await validateUserClaim(functionName, context, "superadmin")
const request = parseRequestPayload<GetAdminUsersRequest>(
functionName,
payload,
)
const adminUserDocs = await db
.collectionGroup("private-user")
.where("roles", "array-contains", request.roleName)
.get()
const admins = adminUserDocs.docs.map((doc) => {
return {
uid: doc.data().uid,
username: doc.data().username,
email: doc.data().email,
roleName: request.roleName,
}
})
return { admins }
} catch (e) {
throw logAndReturnHttpsError(
"internal",
`Firestore ${functionName} not executed. Failed to query admin users. (${
(<Error>e)?.message
})`,
`${functionName}/internal`,
e,
)
}
}
第三,验证辅助函数(需要 Admin SDK)。
export type AdminRoles = "admin" | "superadmin"
export const validateAuthentication = (
functionName: string,
context: CallableContext,
): void => {
if (!context.auth || !context.auth?.uid) {
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. User not authenticated.`,
`${functionName}/unauthenticated`,
)
}
}
export const validateUserClaim = async (
functionName: string,
context: CallableContext,
roleName: AdminRoles,
): Promise<void> => {
if (context.auth?.uid) {
const hasRole = await admin
.auth()
.getUser(context.auth?.uid)
.then((userRecord) => {
return !!userRecord.customClaims?.[roleName]
})
if (hasRole) {
return
}
}
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. User not authenticated as ` +
`'${roleName}'. `,
`${functionName}/unauthenticated`,
)
}
export const validateEmailVerified = async (
functionName: string,
context: CallableContext,
): Promise<void> => {
if (context.auth?.uid) {
const userRecord = await auth.getUser(context.auth?.uid)
if (!userRecord.emailVerified) {
throw logAndReturnHttpsError(
"unauthenticated",
`Firestore ${functionName} not executed. Email is not verified.`,
`${functionName}/email-not-verified`,
)
}
}
}
最后,自定义声明只能在服务器端添加或删除,因为相应的“setCustomUserClaims”函数属于 firebase Admin SDK,而“get-admin-users”函数也可以在客户端实现。 Here 和 here 您将找到有关自定义声明的更多信息,包括受自定义用户声明/角色保护的客户端查询的 Firestore 规则。