-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(apps): create the code of conduct managing functions (#1109)
Create the Firebase functions used for managing the code of conduct application. PR Close #1109
- Loading branch information
1 parent
08f8d67
commit 3f26e9e
Showing
13 changed files
with
332 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
rules_version = '2'; | ||
service cloud.firestore { | ||
match /databases/{database}/documents { | ||
match /{document=**} { | ||
allow read, write: if false | ||
allow read, create: if request.auth != null; | ||
allow update: if request.auth != null && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['blockUntil', 'comments']); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
load("//tools:defaults.bzl", "ts_library") | ||
|
||
package(default_visibility = ["//visibility:private"]) | ||
|
||
ts_library( | ||
name = "code-of-conduct", | ||
srcs = [ | ||
"index.ts", | ||
], | ||
visibility = [ | ||
"//apps/functions:__pkg__", | ||
], | ||
deps = [ | ||
":lib", | ||
"@npm//firebase-admin", | ||
], | ||
) | ||
|
||
ts_library( | ||
name = "lib", | ||
srcs = [ | ||
"blockUser.ts", | ||
"shared.ts", | ||
"syncUsers.ts", | ||
"unblockUser.ts", | ||
], | ||
deps = [ | ||
"@npm//@octokit/auth-app", | ||
"@npm//@octokit/request-error", | ||
"@npm//@octokit/rest", | ||
"@npm//@octokit/webhooks-types", | ||
"@npm//@types/node", | ||
"@npm//firebase-admin", | ||
"@npm//firebase-functions", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import * as functions from 'firebase-functions'; | ||
import * as admin from 'firebase-admin'; | ||
import { | ||
checkAuthenticationAndAccess, | ||
BlockUserParams, | ||
blockedUsersCollection, | ||
getAuthenticatedGithubClient, | ||
} from './shared.js'; | ||
import {RequestError} from '@octokit/request-error'; | ||
|
||
/** Blocks the requested user from Github for the prescribed amount of time. */ | ||
export const blockUser = functions | ||
.runWith({ | ||
secrets: ['ANGULAR_ROBOT_APP_PRIVATE_KEY', 'ANGULAR_ROBOT_APP_ID'], | ||
}) | ||
.https.onCall(async ({comments, blockUntil, context, username}: BlockUserParams, request) => { | ||
// Ensure that the request was authenticated. | ||
checkAuthenticationAndAccess(request); | ||
|
||
/** The Github client for performing Github actions. */ | ||
const github = await getAuthenticatedGithubClient(); | ||
/** The user performing the block action */ | ||
const actor = await admin.auth().getUser(request.auth.uid); | ||
/** The display name of the user. */ | ||
const actorName = actor.displayName || actor.email || 'Unknown User'; | ||
/** The Firestore Document for the user being blocked. */ | ||
const userDoc = await blockedUsersCollection().doc(username).get(); | ||
|
||
if (userDoc.exists) { | ||
throw Error(); | ||
} | ||
|
||
await github.orgs.blockUser({org: 'angular', username: username}).catch((err: RequestError) => { | ||
// If a user is already blocked, we can continue silently failing as action still "succeeded". | ||
if (err.message === 'Blocked user has already been blocked' && err.status === 422) { | ||
return; | ||
} | ||
throw err; | ||
}); | ||
|
||
await userDoc.ref.create({ | ||
comments: comments, | ||
context: context, | ||
username: username, | ||
blockedBy: actorName, | ||
blockedOn: new Date(), | ||
blockUntil: new Date(blockUntil), | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export {blockUser} from './blockUser.js'; | ||
export {unblockUser, dailyUnblock} from './unblockUser.js'; | ||
export {dailySync, syncUsersFromGithub} from './syncUsers.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as admin from 'firebase-admin'; | ||
import {Octokit} from '@octokit/rest'; | ||
import {createAppAuth} from '@octokit/auth-app'; | ||
import * as functions from 'firebase-functions'; | ||
|
||
/** Parameters for blocking a user. */ | ||
export interface BlockUserParams { | ||
username: string; | ||
blockUntil: string; | ||
context: string; | ||
comments: string; | ||
} | ||
|
||
/** Parameters for unblocking a user. */ | ||
export interface UnblockUserParams { | ||
username: string; | ||
} | ||
|
||
/** | ||
* Convertor to ensure the data types for javascript and firestore storage are in sync. | ||
*/ | ||
export const converter: admin.firestore.FirestoreDataConverter<BlockedUser> = { | ||
toFirestore: (user: BlockedUser) => { | ||
return user; | ||
}, | ||
fromFirestore: (data: admin.firestore.QueryDocumentSnapshot<BlockedUser>) => { | ||
return { | ||
username: data.get('username'), | ||
context: data.get('context'), | ||
comments: data.get('comments'), | ||
blockedBy: data.get('blockedBy'), | ||
blockUntil: new Date(data.get('blockUntil')), | ||
blockedOn: new Date(data.get('blockedOn')), | ||
}; | ||
}, | ||
}; | ||
|
||
/** Get the firestore collection for the blocked users, with the converter already set up. */ | ||
export const blockedUsersCollection = () => | ||
admin.firestore().collection('blockedUsers').withConverter(converter); | ||
|
||
/** A blocked user stored in Firestore. */ | ||
export interface BlockedUser extends admin.firestore.DocumentData { | ||
blockedBy: string; | ||
blockedOn: Date; | ||
username: string; | ||
blockUntil: Date; | ||
context: string; | ||
comments: string; | ||
} | ||
|
||
/** A CallableContext which is confirmed to already have an authorized user. */ | ||
interface AuthenticatedCallableContext extends functions.https.CallableContext { | ||
auth: NonNullable<functions.https.CallableContext['auth']>; | ||
} | ||
|
||
/** Verify that the incoming request is authenticated and authorized for access. */ | ||
export function checkAuthenticationAndAccess( | ||
context: functions.https.CallableContext, | ||
): asserts context is AuthenticatedCallableContext { | ||
// Authentication is managed by firebase as this occurs within the Firebase functions context. | ||
// If the user is unauthenticted, the authorization object will be undefined. | ||
if (context.auth == undefined) { | ||
// Throwing an HttpsError so that the client gets the error details. | ||
throw new functions.https.HttpsError('unauthenticated', 'This action requires authentication'); | ||
} | ||
} | ||
|
||
/** Retrieves a Github client instance authenticated as the Angular Robot Github App. */ | ||
export async function getAuthenticatedGithubClient() { | ||
const GITHUB_APP_PEM = Buffer.from( | ||
process.env['ANGULAR_ROBOT_APP_PRIVATE_KEY']!, | ||
'base64', | ||
).toString('utf-8'); | ||
|
||
const applicationGithub = new Octokit({ | ||
authStrategy: createAppAuth, | ||
auth: {appId: process.env['ANGULAR_ROBOT_APP_ID']!, privateKey: GITHUB_APP_PEM}, | ||
}); | ||
/** The specific installation id for the provided repository. */ | ||
const {id: installation_id} = (await applicationGithub.apps.getOrgInstallation({org: 'angular'})) | ||
.data; | ||
/** A temporary github access token. */ | ||
const {token} = ( | ||
await applicationGithub.rest.apps.createInstallationAccessToken({installation_id}) | ||
).data; | ||
|
||
return new Octokit({auth: token}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { | ||
blockedUsersCollection as getBlockedUsersCollection, | ||
getAuthenticatedGithubClient, | ||
checkAuthenticationAndAccess, | ||
} from './shared.js'; | ||
import * as functions from 'firebase-functions'; | ||
|
||
/** Runs the synchronization of blocked users from Github to the blocked users once per day. */ | ||
export const dailySync = functions | ||
.runWith({ | ||
secrets: ['ANGULAR_ROBOT_APP_PRIVATE_KEY', 'ANGULAR_ROBOT_APP_ID'], | ||
}) | ||
.pubsub.schedule('every day 08:00') | ||
.timeZone('America/Los_Angeles') | ||
.onRun(syncUsers); | ||
|
||
/** Runs the synchronization of blocked users from Github to the blocked users list on demand. */ | ||
export const syncUsersFromGithub = functions | ||
.runWith({ | ||
secrets: ['ANGULAR_ROBOT_APP_PRIVATE_KEY', 'ANGULAR_ROBOT_APP_ID'], | ||
}) | ||
.https.onCall(async (_: void, context) => { | ||
await checkAuthenticationAndAccess(context); | ||
await syncUsers(); | ||
}); | ||
|
||
async function syncUsers() { | ||
/** The authenticated Github client for performing actions. */ | ||
const github = await getAuthenticatedGithubClient(); | ||
/** The firestore collection for blocked users */ | ||
const blockedUsersCollection = getBlockedUsersCollection(); | ||
/** A Firestore batch, allowing for atomic updating of the blocked users. */ | ||
const writeBatch = blockedUsersCollection.firestore.batch(); | ||
|
||
/** | ||
* A Date object one year from today, the default block length applied for users discovered | ||
* from Githubs listing. | ||
*/ | ||
const oneYearFromToday = (() => { | ||
const date = new Date(); | ||
date.setFullYear(date.getFullYear() + 1); | ||
return date; | ||
})(); | ||
|
||
/** All of the currently blocked users for the Angular organization. */ | ||
const blockedUsers = await github.paginate(github.orgs.listBlockedUsers, { | ||
org: 'angular', | ||
per_page: 100, | ||
}); | ||
|
||
for (let blockedUser of blockedUsers) { | ||
const firebaseUser = await blockedUsersCollection.doc(blockedUser.login).get(); | ||
// For users we already know about from Github, we skip their records. | ||
if (firebaseUser.exists) { | ||
continue; | ||
} | ||
|
||
writeBatch.create(firebaseUser.ref, { | ||
blockedBy: 'Imported From Github', | ||
blockedOn: new Date(), | ||
blockUntil: oneYearFromToday, | ||
comments: 'This record was automatically imported from Github', | ||
context: 'Unknown', | ||
username: blockedUser.login, | ||
}); | ||
} | ||
await writeBatch.commit(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { | ||
checkAuthenticationAndAccess, | ||
blockedUsersCollection, | ||
UnblockUserParams, | ||
BlockedUser, | ||
getAuthenticatedGithubClient, | ||
} from './shared.js'; | ||
import {Octokit} from '@octokit/rest'; | ||
import * as admin from 'firebase-admin'; | ||
import * as functions from 'firebase-functions'; | ||
|
||
/** Unblocks the provided user from Github, clearing their records from our listing. */ | ||
export const unblockUser = functions | ||
.runWith({ | ||
secrets: ['ANGULAR_ROBOT_APP_PRIVATE_KEY', 'ANGULAR_ROBOT_APP_ID'], | ||
}) | ||
.https.onCall(async ({username}: UnblockUserParams, request) => { | ||
await checkAuthenticationAndAccess(request); | ||
/** The authenticated Github client for performing actions. */ | ||
const github = await getAuthenticatedGithubClient(); | ||
/** The Firestore record of the user to be unblocked */ | ||
const doc = await blockedUsersCollection().doc(username).get(); | ||
|
||
await performUnblock(github, doc); | ||
}); | ||
|
||
/** Unblocks the all listed users who's block has expired, runs daily. */ | ||
export const dailyUnblock = functions | ||
.runWith({ | ||
secrets: ['ANGULAR_ROBOT_APP_PRIVATE_KEY', 'ANGULAR_ROBOT_APP_ID'], | ||
}) | ||
.pubsub.schedule('every day 08:00') | ||
.timeZone('America/Los_Angeles') | ||
.onRun(async () => { | ||
/** The authenticated Github client for performing actions. */ | ||
const github = await getAuthenticatedGithubClient(); | ||
/** The Firestore records for all users who's block has expired. */ | ||
const usersToUnblock = await blockedUsersCollection() | ||
.where('blockUntil', '<', new Date()) | ||
.get(); | ||
|
||
await Promise.all(usersToUnblock.docs.map(async (user) => performUnblock(github, user))); | ||
}); | ||
|
||
async function performUnblock(github: Octokit, doc: admin.firestore.DocumentSnapshot<BlockedUser>) { | ||
await github.orgs | ||
.unblockUser({org: 'angular', username: doc.get('username')}) | ||
.then(() => doc.ref.delete()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export * from './githubWebhook/index.js'; | ||
export * from './ng-dev/index.js'; | ||
export * from './code-of-conduct/index.js'; | ||
import * as admin from 'firebase-admin'; | ||
|
||
admin.initializeApp(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.